unfortunately the compiler does not consult the "impl" of a type in order to acquire bind_expression() and column_expression(). This will require major changes that I can only target at 1.2.
bind_expression, column_expression need to work per dialect impl, work for TypeDecorator, Variant
When using with_variant()
to specify a custom column (decorated type) for the dialect variant, implementations of bind_expression
and col_expression
are never called.
Attached file test.py
contains a minimal example showing this in action.
Expected behaviour is that the INSERT query fails as it uses functions unavailable in SQLite, however bind_expression
and col_expression
are never called to make the SQL-level changes to apply these functions.
Obviously I'm not expecting the INSERT queries to actually work in SQLite because those functions don't exist, but I've written the example this way to demonstrate that the SQL-level overrides are never getting called.
The real world use case for this is that I'm trying to have a variant type apply for MySQL that uses COMPRESS()
and UNCOMPRESS()
while for local testing with SQLite the data is stored unchanged.
SQLAlchemy version: 1.1.9
Reproduced in: - Python 2.7.10 on Mac OS X 10.11.6 - Python 2.7.3 on Debian Wheezy
Official response
Comments (10)
-
repo owner -
repo owner -
reporter So, am I right in thinking that it's specifically the use of Variant here that's the problem? I got as far as finding that
_has_col_expression
and_has_bind_expression
were returningFalse
when using Variant, which I assumed was why the overrides weren't getting called?In the mean time, can you recommend another way of accomplishing this?
Regards.
-
repo owner - changed milestone to 1.3
- changed title to bind_expression, column_expression need to work per dialect impl, work for TypeDecorator, Variant
- marked as critical
not sure which route to take:
method one
the dialect should be passed to bind_expression() and column_expression(). In order to deal with backwards incompatibility, compiler will invoke type._compat_bind_expression and type._compat_column_expression, each of which do an inspect() on the real method and correct for the "dialect" argument missing, emit a warning, and cache the result on the type itself. TypeDecorator, or at the very least Variant, will start implementing bind_expression() and column_expression() and begin routing these methods to the "impl" in the absense of the user-defined
method two
compiler will begin consulting "type" and "impl" directly in _label_select_column and visit_bindparam(). The current logic that tests the type only will need to be expanded to accommodate for both type and dialect-specific impl - it will invoke first for dialect-specific impl and if an expression is returned, that is then wrapped in the one returned by dialect-agnostic impl.
diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 6da0647..5161d2f 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -1213,10 +1213,19 @@ class SQLCompiler(Compiled): literal_binds=False, skip_bind_expression=False, **kwargs): - if not skip_bind_expression and bindparam.type._has_bind_expression: - bind_expression = bindparam.type.bind_expression(bindparam) - return self.process(bind_expression, - skip_bind_expression=True) + if not skip_bind_expression: + impl = bindparam.type.dialect_impl(self.dialect) + + if bindparam.type._has_bind_expression: + bind_expression = bindparam.type.bind_expression( + impl.bind_expression(bindparam) + if impl._has_bind_expression else + bindparam + ) + return self.process(bind_expression, skip_bind_expression=True) + elif impl._has_bind_expression: + bind_expression = impl.bind_expression(bindparam) + return self.process(bind_expression, skip_bind_expression=True) if literal_binds or \ (within_columns_clause and
at the moment option 2 seems better because it allows for dialect-specific impls to provide bind / column expressions too.
-
repo owner workaround for your case is to use custom compilation constructs:
#!/usr/bin/env python import sqlalchemy import sqlalchemy.ext.declarative import sqlalchemy.orm import sqlalchemy.types from sqlalchemy.ext.compiler import compiles from sqlalchemy.sql import ColumnElement class Compress(ColumnElement): def __init__(self, element, is_compress, type_): self.element = element self.is_compress = is_compress self.type_ = type_ @compiles(Compress) def _default_compress(element, compiler, **kw): return compiler.process(element.element, **kw) @compiles(Compress, "sqlite") def _sqlite_compress(element, compiler, **kw): return compiler.process( sqlalchemy.func.compress(element.element, type_=element.type_) if element.is_compress else sqlalchemy.func.uncompress(element.element, type_=element.type_), **kw ) class CompressedLargeBinary(sqlalchemy.types.TypeDecorator): impl = sqlalchemy.types.LargeBinary def bind_expression(self, bindvalue): return Compress(bindvalue, True, self.impl) def column_expression(self, col): return Compress(col, False, self.impl) Base = sqlalchemy.ext.declarative.declarative_base() class Foo(Base): __tablename__ = "foo" id = sqlalchemy.Column(sqlalchemy.Integer, nullable=False, primary_key=True) bar = sqlalchemy.Column("baz", CompressedLargeBinary, nullable=False) def __init__(self, bar): self.id = None self.bar = bar from sqlalchemy.dialects import sqlite from sqlalchemy import select stmt = select([Foo.__table__]).where(Foo.bar == 'asdf') print stmt.compile(dialect=sqlite.dialect()) stmt = Foo.__table__.insert().values(baz='xyz') print stmt.compile(dialect=sqlite.dialect())
-
repo owner Variant makes use of TypeDecorator and "impl" to switch between datatypes, so yes, Variant can only take effect for those places where the Core takes the time to look at the "impl". Historically this has not been within Compiler and the bind_expression() / col_expression() features, added much later, did not anticipate this.
-
reporter Your workaround works perfectly, thanks!
One other thing (though happy to raise a separate issue for this if required and I've already seen a few similar-looking issues): Having put the workaround into place in the real application, when I retrieve the uncompressed value from SQLite it's coming back as a
buffer
instance instead ofstr
. I tried overridingprocess_result_value
inCompressedLargeBinary
to convert tostr
however now that never gets called. Did I miss something obvious? -
reporter Just managed to resolve the above (secondary) issue with
buffer
andstr
. After double-checking the documentation at http://docs.sqlalchemy.org/en/latest/core/compiler.html, specifically the section that reads "ColumnElement classes want to have a type member which is expression’s return type.", I changed this (from your workaround), however I had to keeptype_
as otherwise this fails when using MySQL:class Compress(sqlalchemy.sql.ColumnElement): def __init__(self, element, is_compress, type_): self.element = element self.is_compress = is_compress self.type_ = type_
To this:
class Compress(sqlalchemy.sql.ColumnElement): def __init__(self, element, is_compress, type_): self.element = element self.is_compress = is_compress self.type = type_ # Needed to work with SQLite so that str is returned self.type_ = type_ # Needed to work with MySQL
Is it valid to have type and type_ together?
EDIT:
Actually, I see where
type
is referenced now, so I've updated the code I'm using to look like the following:The two changes are:
- Using
type
insteadtype_
- Passing
sqlalchemy.func.compress
andsqlalchemy.func.uncompress
in instead of using a bool to select which to use
import sqlalchemy.sql import sqlalchemy.types import sqlalchemy.ext.compiler class Compress(sqlalchemy.sql.ColumnElement): def __init__(self, element, compress_func, type_): self.element = element self.compress_func = compress_func self.type = type_ @sqlalchemy.ext.compiler.compiles(Compress) def _default_compress(element, compiler, **kw): return compiler.process(element.element, **kw) @sqlalchemy.ext.compiler.compiles(Compress, "mysql") def _sqlite_compress(element, compiler, **kw): return compiler.process(element.compress_func(element.element, type_=element.type), **kw) class CompressedLargeBinary(sqlalchemy.types.TypeDecorator): impl = sqlalchemy.types.LargeBinary def bind_expression(self, bindvalue): return Compress(bindvalue, sqlalchemy.func.compress, self.impl) def column_expression(self, col): return Compress(col, sqlalchemy.func.uncompress, self.impl)
- Using
-
repo owner yeah that is all great, sorry about the type / type_ thing.
-
repo owner please implement using method two and allow for dialect-level bind_expression as well as nesting. See #4036.
- Log in to comment
workaround for your case is to use custom compilation constructs: