diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/sqlalchemy/dialects/mssql/base.py | 4 | ||||
| -rw-r--r-- | lib/sqlalchemy/engine/create.py | 32 | ||||
| -rw-r--r-- | lib/sqlalchemy/engine/default.py | 186 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/coercions.py | 40 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/compiler.py | 222 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/elements.py | 44 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/operators.py | 28 | ||||
| -rw-r--r-- | lib/sqlalchemy/testing/assertions.py | 22 |
8 files changed, 332 insertions, 246 deletions
diff --git a/lib/sqlalchemy/dialects/mssql/base.py b/lib/sqlalchemy/dialects/mssql/base.py index ded90c348..cf703a363 100644 --- a/lib/sqlalchemy/dialects/mssql/base.py +++ b/lib/sqlalchemy/dialects/mssql/base.py @@ -1909,14 +1909,14 @@ class MSSQLStrictCompiler(MSSQLCompiler): ansi_bind_rules = True def visit_in_op_binary(self, binary, operator, **kw): - kw["literal_binds"] = True + kw["literal_execute"] = True return "%s IN %s" % ( self.process(binary.left, **kw), self.process(binary.right, **kw), ) def visit_notin_op_binary(self, binary, operator, **kw): - kw["literal_binds"] = True + kw["literal_execute"] = True return "%s NOT IN %s" % ( self.process(binary.left, **kw), self.process(binary.right, **kw), diff --git a/lib/sqlalchemy/engine/create.py b/lib/sqlalchemy/engine/create.py index fd6105561..1378a6799 100644 --- a/lib/sqlalchemy/engine/create.py +++ b/lib/sqlalchemy/engine/create.py @@ -23,7 +23,16 @@ from .. import util ":func:`.create_mock_engine` going forward. For general " "customization of create_engine which may have been accomplished " "using strategies, see :class:`.CreateEnginePlugin`.", - ) + ), + empty_in_strategy=( + "1.4", + "The :paramref:`.create_engine.empty_in_strategy` keyword is " + "deprecated, and no longer has any effect. All IN expressions " + "are now rendered using " + 'the "expanding parameter" strategy which renders a set of bound' + 'expressions, or an "empty set" SELECT, at statement execution' + "time.", + ), ) def create_engine(url, **kwargs): """Create a new :class:`.Engine` instance. @@ -130,23 +139,8 @@ def create_engine(url, **kwargs): logging. - :param empty_in_strategy: The SQL compilation strategy to use when - rendering an IN or NOT IN expression for :meth:`.ColumnOperators.in_` - where the right-hand side - is an empty set. This is a string value that may be one of - ``static``, ``dynamic``, or ``dynamic_warn``. The ``static`` - strategy is the default, and an IN comparison to an empty set - will generate a simple false expression "1 != 1". The ``dynamic`` - strategy behaves like that of SQLAlchemy 1.1 and earlier, emitting - a false expression of the form "expr != expr", which has the effect - of evaluting to NULL in the case of a null expression. - ``dynamic_warn`` is the same as ``dynamic``, however also emits a - warning when an empty set is encountered; this because the "dynamic" - comparison is typically poorly performing on most databases. - - .. versionadded:: 1.2 Added the ``empty_in_strategy`` setting and - additionally defaulted the behavior for empty-set IN comparisons - to a static boolean expression. + :param empty_in_strategy: No longer used; SQLAlchemy now uses + "empty set" behavior for IN in all cases. :param encoding: Defaults to ``utf-8``. This is the string encoding used by SQLAlchemy for string encode/decode @@ -412,6 +406,8 @@ def create_engine(url, **kwargs): else: raise exc.ArgumentError("unknown strategy: %r" % strat) + kwargs.pop("empty_in_strategy", None) + # create url.URL object u = _url.make_url(url) diff --git a/lib/sqlalchemy/engine/default.py b/lib/sqlalchemy/engine/default.py index 9eacf0527..7016adcf4 100644 --- a/lib/sqlalchemy/engine/default.py +++ b/lib/sqlalchemy/engine/default.py @@ -192,7 +192,16 @@ class DefaultDialect(interfaces.Dialect): "and corresponding dialect-level parameters are deprecated, " "and will be removed in a future release. Modern DBAPIs support " "Python Unicode natively and this parameter is unnecessary.", - ) + ), + empty_in_strategy=( + "1.4", + "The :paramref:`.create_engine.empty_in_strategy` keyword is " + "deprecated, and no longer has any effect. All IN expressions " + "are now rendered using " + 'the "expanding parameter" strategy which renders a set of bound' + 'expressions, or an "empty set" SELECT, at statement execution' + "time.", + ), ) def __init__( self, @@ -203,7 +212,6 @@ class DefaultDialect(interfaces.Dialect): implicit_returning=None, case_sensitive=True, supports_native_boolean=None, - empty_in_strategy="static", max_identifier_length=None, label_length=None, **kwargs @@ -235,18 +243,6 @@ class DefaultDialect(interfaces.Dialect): self.supports_native_boolean = supports_native_boolean self.case_sensitive = case_sensitive - self.empty_in_strategy = empty_in_strategy - if empty_in_strategy == "static": - self._use_static_in = True - elif empty_in_strategy in ("dynamic", "dynamic_warn"): - self._use_static_in = False - self._warn_on_empty_in = empty_in_strategy == "dynamic_warn" - else: - raise exc.ArgumentError( - "empty_in_strategy may be 'static', " - "'dynamic', or 'dynamic_warn'" - ) - self._user_defined_max_identifier_length = max_identifier_length if self._user_defined_max_identifier_length: self.max_identifier_length = ( @@ -732,19 +728,18 @@ class DefaultExecutionContext(interfaces.ExecutionContext): compiled._loose_column_name_matching, ) - self.unicode_statement = util.text_type(compiled) - if not dialect.supports_unicode_statements: - self.statement = self.unicode_statement.encode( - self.dialect.encoding - ) - else: - self.statement = self.unicode_statement - self.isinsert = compiled.isinsert self.isupdate = compiled.isupdate self.isdelete = compiled.isdelete self.is_text = compiled.isplaintext + if self.isinsert or self.isupdate or self.isdelete: + self.is_crud = True + self._is_explicit_returning = bool(compiled.statement._returning) + self._is_implicit_returning = bool( + compiled.returning and not compiled.statement._returning + ) + if not parameters: self.compiled_parameters = [compiled.construct_params()] else: @@ -755,14 +750,11 @@ class DefaultExecutionContext(interfaces.ExecutionContext): self.executemany = len(parameters) > 1 - self.cursor = self.create_cursor() + # this must occur before create_cursor() since the statement + # has to be regexed in some cases for server side cursor + self.unicode_statement = util.text_type(compiled) - if self.isinsert or self.isupdate or self.isdelete: - self.is_crud = True - self._is_explicit_returning = bool(compiled.statement._returning) - self._is_implicit_returning = bool( - compiled.returning and not compiled.statement._returning - ) + self.cursor = self.create_cursor() if self.compiled.insert_prefetch or self.compiled.update_prefetch: if self.executemany: @@ -772,15 +764,38 @@ class DefaultExecutionContext(interfaces.ExecutionContext): processors = compiled._bind_processors - if compiled.literal_execute_params: - # copy processors for this case as they will be mutated - processors = dict(processors) - positiontup = self._literal_execute_parameters( - compiled, processors + if compiled.literal_execute_params or compiled.post_compile_params: + if self.executemany: + raise exc.InvalidRequestError( + "'literal_execute' or 'expanding' parameters can't be " + "used with executemany()" + ) + + expanded_state = compiled._process_parameters_for_postcompile( + self.compiled_parameters[0] ) + + # re-assign self.unicode_statement + self.unicode_statement = expanded_state.statement + + # used by set_input_sizes() which is needed for Oracle + self._expanded_parameters = expanded_state.parameter_expansion + + processors = dict(processors) + processors.update(expanded_state.processors) + positiontup = expanded_state.positiontup elif compiled.positional: positiontup = self.compiled.positiontup + # final self.unicode_statement is now assigned, encode if needed + # by dialect + if not dialect.supports_unicode_statements: + self.statement = self.unicode_statement.encode( + self.dialect.encoding + ) + else: + self.statement = self.unicode_statement + # Convert the dictionary of bind parameter values # into a dict or list to be sent to the DBAPI's # execute() or executemany() method. @@ -825,105 +840,6 @@ class DefaultExecutionContext(interfaces.ExecutionContext): return self - def _literal_execute_parameters(self, compiled, processors): - """handle special post compile parameters. - - These include: - - * "expanding" parameters -typically IN tuples that are rendered - on a per-parameter basis for an otherwise fixed SQL statement string. - - * literal_binds compiled with the literal_execute flag. Used for - things like SQL Server "TOP N" where the driver does not accommodate - N as a bound parameter. - - """ - if self.executemany: - raise exc.InvalidRequestError( - "'literal_execute' or 'expanding' parameters can't be " - "used with executemany()" - ) - - if compiled.positional and compiled._numeric_binds: - # I'm not familiar with any DBAPI that uses 'numeric'. - # strategy would likely be to make use of numbers greater than - # the highest number present; then for expanding parameters, - # append them to the end of the parameter list. that way - # we avoid having to renumber all the existing parameters. - raise NotImplementedError( - "'post-compile' bind parameters are not supported with " - "the 'numeric' paramstyle at this time." - ) - - self._expanded_parameters = {} - - compiled_params = self.compiled_parameters[0] - if compiled.positional: - positiontup = [] - else: - positiontup = None - - replacement_expressions = {} - to_update_sets = {} - - for name in ( - compiled.positiontup - if compiled.positional - else compiled.bind_names.values() - ): - parameter = compiled.binds[name] - if parameter in compiled.literal_execute_params: - - if not parameter.expanding: - value = compiled_params.pop(name) - replacement_expressions[ - name - ] = compiled.render_literal_bindparam( - parameter, render_literal_value=value - ) - continue - - if name in replacement_expressions: - to_update = to_update_sets[name] - else: - # we are removing the parameter from compiled_params - # because it is a list value, which is not expected by - # TypeEngine objects that would otherwise be asked to - # process it. the single name is being replaced with - # individual numbered parameters for each value in the - # param. - values = compiled_params.pop(name) - - leep = compiled._literal_execute_expanding_parameter - to_update, replacement_expr = leep(name, parameter, values) - - to_update_sets[name] = to_update - replacement_expressions[name] = replacement_expr - - if not parameter.literal_execute: - compiled_params.update(to_update) - - processors.update( - (key, processors[name]) - for key, value in to_update - if name in processors - ) - if compiled.positional: - positiontup.extend(name for name, value in to_update) - self._expanded_parameters[name] = [ - expand_key for expand_key, value in to_update - ] - elif compiled.positional: - positiontup.append(name) - - def process_expanding(m): - return replacement_expressions[m.group(1)] - - self.statement = re.sub( - r"\[POSTCOMPILE_(\S+)\]", process_expanding, self.statement - ) - return positiontup - @classmethod def _init_statement( cls, dialect, connection, dbapi_connection, statement, parameters @@ -1084,8 +1000,8 @@ class DefaultExecutionContext(interfaces.ExecutionContext): self.compiled.statement, expression.TextClause ) ) - and self.statement - and SERVER_SIDE_CURSOR_RE.match(self.statement) + and self.unicode_statement + and SERVER_SIDE_CURSOR_RE.match(self.unicode_statement) ) ) ) diff --git a/lib/sqlalchemy/sql/coercions.py b/lib/sqlalchemy/sql/coercions.py index b45ef3991..97524bc6a 100644 --- a/lib/sqlalchemy/sql/coercions.py +++ b/lib/sqlalchemy/sql/coercions.py @@ -331,20 +331,29 @@ class InElementImpl(RoleImpl, roles.InElementRole): if isinstance(element, collections_abc.Iterable) and not isinstance( element, util.string_types ): - args = [] + non_literal_expressions = {} + element = list(element) for o in element: if not _is_literal(o): if not isinstance(o, operators.ColumnOperators): self._raise_for_expected(element, **kw) + else: + non_literal_expressions[o] = o elif o is None: - o = elements.Null() - else: - o = expr._bind_param(operator, o) - args.append(o) - - return elements.ClauseList( - _tuple_values=isinstance(expr, elements.Tuple), *args - ) + non_literal_expressions[o] = elements.Null() + + if non_literal_expressions: + return elements.ClauseList( + _tuple_values=isinstance(expr, elements.Tuple), + *[ + non_literal_expressions[o] + if o in non_literal_expressions + else expr._bind_param(operator, o) + for o in element + ] + ) + else: + return expr._bind_param(operator, element, expanding=True) else: self._raise_for_expected(element, **kw) @@ -353,17 +362,8 @@ class InElementImpl(RoleImpl, roles.InElementRole): if element._is_select_statement: return element.scalar_subquery() elif isinstance(element, elements.ClauseList): - if len(element.clauses) == 0: - op, negate_op = ( - (operators.empty_in_op, operators.empty_notin_op) - if operator is operators.in_op - else (operators.empty_notin_op, operators.empty_in_op) - ) - return element.self_group(against=op)._annotate( - dict(in_ops=(op, negate_op)) - ) - else: - return element.self_group(against=operator) + assert not len(element.clauses) == 0 + return element.self_group(against=operator) elif isinstance(element, elements.BindParameter) and element.expanding: if isinstance(expr, elements.Tuple): diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 807b01c24..75ccad3fd 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -23,6 +23,7 @@ To generate user-defined SQL strings, see """ +import collections import contextlib import itertools import re @@ -257,6 +258,18 @@ RM_OBJECTS = 2 RM_TYPE = 3 +ExpandedState = collections.namedtuple( + "ExpandedState", + [ + "statement", + "additional_parameters", + "processors", + "positiontup", + "parameter_expansion", + ], +) + + class Compiled(object): """Represent a compiled SQL or DDL expression. @@ -525,6 +538,12 @@ class SQLCompiler(Compiled): """ + _render_postcompile = False + """ + whether to render out POSTCOMPILE params during the compile phase. + + """ + insert_single_values_expr = None """When an INSERT is compiled with a single set of parameters inside a VALUES expression, the string is assigned here, where it can be @@ -535,6 +554,16 @@ class SQLCompiler(Compiled): """ literal_execute_params = frozenset() + """bindparameter objects that are rendered as literal values at statement + execution time. + + """ + + post_compile_params = frozenset() + """bindparameter objects that are rendered as bound parameter placeholders + at statement execution time. + + """ insert_prefetch = update_prefetch = () @@ -610,6 +639,9 @@ class SQLCompiler(Compiled): if self.positional and self._numeric_binds: self._apply_numbered_params() + if self._render_postcompile: + self._process_parameters_for_postcompile(_populate_self=True) + @property def prefetch(self): return list(self.insert_prefetch + self.update_prefetch) @@ -665,7 +697,12 @@ class SQLCompiler(Compiled): for key, value in ( ( self.bind_names[bindparam], - bindparam.type._cached_bind_processor(self.dialect), + bindparam.type._cached_bind_processor(self.dialect) + if not bindparam._expanding_in_types + else tuple( + elem_type._cached_bind_processor(self.dialect) + for elem_type in bindparam._expanding_in_types + ), ) for bindparam in self.bind_names ) @@ -741,6 +778,141 @@ class SQLCompiler(Compiled): compiled object, for those values that are present.""" return self.construct_params(_check=False) + def _process_parameters_for_postcompile( + self, parameters=None, _populate_self=False + ): + """handle special post compile parameters. + + These include: + + * "expanding" parameters -typically IN tuples that are rendered + on a per-parameter basis for an otherwise fixed SQL statement string. + + * literal_binds compiled with the literal_execute flag. Used for + things like SQL Server "TOP N" where the driver does not accommodate + N as a bound parameter. + + """ + + if parameters is None: + parameters = self.construct_params() + + expanded_parameters = {} + if self.positional: + positiontup = [] + else: + positiontup = None + + processors = self._bind_processors + + new_processors = {} + + if self.positional and self._numeric_binds: + # I'm not familiar with any DBAPI that uses 'numeric'. + # strategy would likely be to make use of numbers greater than + # the highest number present; then for expanding parameters, + # append them to the end of the parameter list. that way + # we avoid having to renumber all the existing parameters. + raise NotImplementedError( + "'post-compile' bind parameters are not supported with " + "the 'numeric' paramstyle at this time." + ) + + replacement_expressions = {} + to_update_sets = {} + + for name in ( + self.positiontup if self.positional else self.bind_names.values() + ): + parameter = self.binds[name] + if parameter in self.literal_execute_params: + value = parameters.pop(name) + replacement_expressions[name] = self.render_literal_bindparam( + parameter, render_literal_value=value + ) + continue + + if parameter in self.post_compile_params: + if name in replacement_expressions: + to_update = to_update_sets[name] + else: + # we are removing the parameter from parameters + # because it is a list value, which is not expected by + # TypeEngine objects that would otherwise be asked to + # process it. the single name is being replaced with + # individual numbered parameters for each value in the + # param. + values = parameters.pop(name) + + leep = self._literal_execute_expanding_parameter + to_update, replacement_expr = leep(name, parameter, values) + + to_update_sets[name] = to_update + replacement_expressions[name] = replacement_expr + + if not parameter.literal_execute: + parameters.update(to_update) + if parameter._expanding_in_types: + new_processors.update( + ( + "%s_%s_%s" % (name, i, j), + processors[name][j - 1], + ) + for i, tuple_element in enumerate(values, 1) + for j, value in enumerate(tuple_element, 1) + if name in processors + and processors[name][j - 1] is not None + ) + else: + new_processors.update( + (key, processors[name]) + for key, value in to_update + if name in processors + ) + if self.positional: + positiontup.extend(name for name, value in to_update) + expanded_parameters[name] = [ + expand_key for expand_key, value in to_update + ] + elif self.positional: + positiontup.append(name) + + def process_expanding(m): + return replacement_expressions[m.group(1)] + + statement = re.sub( + r"\[POSTCOMPILE_(\S+)\]", process_expanding, self.string + ) + + expanded_state = ExpandedState( + statement, + parameters, + new_processors, + positiontup, + expanded_parameters, + ) + + if _populate_self: + # this is for the "render_postcompile" flag, which is not + # otherwise used internally and is for end-user debugging and + # special use cases. + self.string = expanded_state.statement + self._bind_processors.update(expanded_state.processors) + self.positiontup = expanded_state.positiontup + self.post_compile_params = frozenset() + for key in expanded_state.parameter_expansion: + bind = self.binds.pop(key) + self.bind_names.pop(bind) + for value, expanded_key in zip( + bind.value, expanded_state.parameter_expansion[key] + ): + self.binds[expanded_key] = new_param = bind._with_value( + value + ) + self.bind_names[new_param] = expanded_key + + return expanded_state + @util.dependencies("sqlalchemy.engine.result") def _create_result_map(self, result): """utility method used for unit tests only.""" @@ -1291,31 +1463,6 @@ class SQLCompiler(Compiled): binary, override_operator=operators.match_op ) - def _emit_empty_in_warning(self): - util.warn( - "The IN-predicate was invoked with an " - "empty sequence. This results in a " - "contradiction, which nonetheless can be " - "expensive to evaluate. Consider alternative " - "strategies for improved performance." - ) - - def visit_empty_in_op_binary(self, binary, operator, **kw): - if self.dialect._use_static_in: - return "1 != 1" - else: - if self.dialect._warn_on_empty_in: - self._emit_empty_in_warning() - return self.process(binary.left != binary.left) - - def visit_empty_notin_op_binary(self, binary, operator, **kw): - if self.dialect._use_static_in: - return "1 = 1" - else: - if self.dialect._warn_on_empty_in: - self._emit_empty_in_warning() - return self.process(binary.left == binary.left) - def visit_empty_set_expr(self, element_types): raise NotImplementedError( "Dialect '%s' does not support empty set expression." @@ -1407,7 +1554,7 @@ class SQLCompiler(Compiled): and isinstance(binary.left, elements.BindParameter) and isinstance(binary.right, elements.BindParameter) ): - kw["literal_binds"] = True + kw["literal_execute"] = True operator_ = override_operator or binary.operator disp = self._get_operator_dispatch(operator_, "binary", None) @@ -1588,6 +1735,7 @@ class SQLCompiler(Compiled): literal_binds=False, skip_bind_expression=False, literal_execute=False, + render_postcompile=False, **kwargs ): @@ -1605,17 +1753,16 @@ class SQLCompiler(Compiled): ) if not literal_binds: - post_compile = ( + literal_execute = ( literal_execute or bindparam.literal_execute - or bindparam.expanding + or (within_columns_clause and self.ansi_bind_rules) ) + post_compile = literal_execute or bindparam.expanding else: post_compile = False - if not literal_execute and ( - literal_binds or (within_columns_clause and self.ansi_bind_rules) - ): + if not literal_execute and (literal_binds): ret = self.render_literal_bindparam( bindparam, within_columns_clause=True, **kwargs ) @@ -1650,7 +1797,13 @@ class SQLCompiler(Compiled): self.binds[bindparam.key] = self.binds[name] = bindparam if post_compile: - self.literal_execute_params |= {bindparam} + if render_postcompile: + self._render_postcompile = True + + if literal_execute: + self.literal_execute_params |= {bindparam} + else: + self.post_compile_params |= {bindparam} ret = self.bindparam_string( name, @@ -2897,6 +3050,9 @@ class StrSQLCompiler(SQLCompiler): for t in extra_froms ) + def visit_empty_set_expr(self, type_): + return "SELECT 1 WHERE 1!=1" + class DDLCompiler(Compiled): @util.memoized_property diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index 464c2a4d6..7d857d4fe 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -759,7 +759,7 @@ class ColumnElement( def reverse_operate(self, op, other, **kwargs): return op(other, self.comparator, **kwargs) - def _bind_param(self, operator, obj, type_=None): + def _bind_param(self, operator, obj, type_=None, expanding=False): return BindParameter( None, obj, @@ -767,6 +767,7 @@ class ColumnElement( type_=type_, _compared_to_type=self.type, unique=True, + expanding=expanding, ) @property @@ -1281,7 +1282,6 @@ class BindParameter(roles.InElementRole, ColumnElement): self.required = required self.expanding = expanding self.literal_execute = literal_execute - if type_ is None: if _compared_to_type is not None: self.type = _compared_to_type.coerce_compared_value( @@ -2282,20 +2282,29 @@ class Tuple(ClauseList, ColumnElement): def _select_iterable(self): return (self,) - def _bind_param(self, operator, obj, type_=None): - return Tuple( - *[ - BindParameter( - None, - o, - _compared_to_operator=operator, - _compared_to_type=compared_to_type, - unique=True, - type_=type_, - ) - for o, compared_to_type in zip(obj, self._type_tuple) - ] - ).self_group() + def _bind_param(self, operator, obj, type_=None, expanding=False): + if expanding: + return BindParameter( + None, + value=obj, + _compared_to_operator=operator, + unique=True, + expanding=True, + )._with_expanding_in_types(self._type_tuple) + else: + return Tuple( + *[ + BindParameter( + None, + o, + _compared_to_operator=operator, + _compared_to_type=compared_to_type, + unique=True, + type_=type_, + ) + for o, compared_to_type in zip(obj, self._type_tuple) + ] + ).self_group() class Case(ColumnElement): @@ -4240,7 +4249,7 @@ class ColumnClause( else: return name - def _bind_param(self, operator, obj, type_=None): + def _bind_param(self, operator, obj, type_=None, expanding=False): return BindParameter( self.key, obj, @@ -4248,6 +4257,7 @@ class ColumnClause( _compared_to_type=self.type, type_=type_, unique=True, + expanding=expanding, ) def _make_proxy( diff --git a/lib/sqlalchemy/sql/operators.py b/lib/sqlalchemy/sql/operators.py index 3aeaaa601..22bf3d150 100644 --- a/lib/sqlalchemy/sql/operators.py +++ b/lib/sqlalchemy/sql/operators.py @@ -538,17 +538,15 @@ class ColumnOperators(Operators): stmt.where(column.in_([])) - In this calling form, the expression renders a "false" expression, - e.g.:: + In this calling form, the expression renders an "empty set" + expression. These expressions are tailored to individual backends + and are generaly trying to get an empty SELECT statement as a + subuqery. Such as on SQLite, the expression is:: - WHERE 1 != 1 + WHERE col IN (SELECT 1 FROM (SELECT 1) WHERE 1!=1) - This "false" expression has historically had different behaviors - in older SQLAlchemy versions, see - :paramref:`.create_engine.empty_in_strategy` for behavioral options. - - .. versionchanged:: 1.2 simplified the behavior of "empty in" - expressions + .. versionchanged:: 1.4 empty IN expressions now use an + execution-time generated SELECT subquery in all cases. * A bound parameter, e.g. :func:`.bindparam`, may be used if it includes the :paramref:`.bindparam.expanding` flag:: @@ -1341,16 +1339,6 @@ def comma_op(a, b): raise NotImplementedError() -@comparison_op -def empty_in_op(a, b): - raise NotImplementedError() - - -@comparison_op -def empty_notin_op(a, b): - raise NotImplementedError() - - def filter_op(a, b): raise NotImplementedError() @@ -1473,8 +1461,6 @@ _PRECEDENCE = { ne: 5, is_distinct_from: 5, isnot_distinct_from: 5, - empty_in_op: 5, - empty_notin_op: 5, gt: 5, lt: 5, ge: 5, diff --git a/lib/sqlalchemy/testing/assertions.py b/lib/sqlalchemy/testing/assertions.py index f057ae37b..563dc2a24 100644 --- a/lib/sqlalchemy/testing/assertions.py +++ b/lib/sqlalchemy/testing/assertions.py @@ -340,12 +340,15 @@ class AssertsCompiledSQL(object): result, params=None, checkparams=None, + check_literal_execute=None, + check_post_param=None, dialect=None, checkpositional=None, check_prefetch=None, use_default_dialect=False, allow_dialect_select=False, literal_binds=False, + render_postcompile=False, schema_translate_map=None, ): if use_default_dialect: @@ -377,6 +380,9 @@ class AssertsCompiledSQL(object): if literal_binds: compile_kwargs["literal_binds"] = True + if render_postcompile: + compile_kwargs["render_postcompile"] = True + if isinstance(clause, orm.Query): context = clause._compile_context() context.statement.use_labels = True @@ -418,6 +424,22 @@ class AssertsCompiledSQL(object): eq_(tuple([p[x] for x in c.positiontup]), checkpositional) if check_prefetch is not None: eq_(c.prefetch, check_prefetch) + if check_literal_execute is not None: + eq_( + { + c.bind_names[b]: b.effective_value + for b in c.literal_execute_params + }, + check_literal_execute, + ) + if check_post_param is not None: + eq_( + { + c.bind_names[b]: b.effective_value + for b in c.post_compile_params + }, + check_post_param, + ) class ComparesTables(object): |
