summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/sqlalchemy/dialects/mssql/base.py4
-rw-r--r--lib/sqlalchemy/engine/create.py32
-rw-r--r--lib/sqlalchemy/engine/default.py186
-rw-r--r--lib/sqlalchemy/sql/coercions.py40
-rw-r--r--lib/sqlalchemy/sql/compiler.py222
-rw-r--r--lib/sqlalchemy/sql/elements.py44
-rw-r--r--lib/sqlalchemy/sql/operators.py28
-rw-r--r--lib/sqlalchemy/testing/assertions.py22
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):