summaryrefslogtreecommitdiff
path: root/lib/sqlalchemy/sql
diff options
context:
space:
mode:
authorMike Bayer <mike_mp@zzzcomputing.com>2020-03-06 16:04:46 -0500
committerMike Bayer <mike_mp@zzzcomputing.com>2020-03-10 16:55:03 -0400
commit693938dd6fb2f3ee3e031aed4c62355ac97f3ceb (patch)
tree94701d7df1b7274151800efd6ca996e1f4203916 /lib/sqlalchemy/sql
parent851fb8f5a661c66ee76308181118369c8c4df9e0 (diff)
downloadsqlalchemy-693938dd6fb2f3ee3e031aed4c62355ac97f3ceb.tar.gz
Rework select(), CompoundSelect() in terms of CompileState
Continuation of I408e0b8be91fddd77cf279da97f55020871f75a9 - add an options() method to the base Generative construct. this will be where ORM options can go - Change Null, False_, True_ to be singletons, so that we aren't instantiating them and having to use isinstance. The previous issue with this was that they would produce dupe labels in SELECT statements. Apply the duplicate column logic, newly added in 1.4, to these objects as well as to non-apply-labels SELECT statements in general as a means of improving this. - create a revised system for generating ClauseList compilation constructs that simplfies up front creation to not actually use ClauseList; a simple tuple is rendered by the compiler using the same constrcution rules as what are used for ClauseList but without creating the actual object. Apply to Select, CompoundSelect, revise Update, Delete - Select, CompoundSelect get an initial CompileState implementation. All methods used only within compilation are moved here - refine update/insert/delete compile state to not require an outside boolean - refine and simplify Select._copy_internals - rework bind(), which is going away, to not use some of the internal traversal stuff - remove "autocommit", "for_update" parameters from Select, references #4643 - remove "autocommit" parameter from TextClause , references #4643 - add deprecation warnings for statement.execute(), engine.execute(), statement.scalar(), engine.scalar(). Fixes: #5193 Change-Id: I04ca0152b046fd42c5054ba10f37e43fc6e5a57b
Diffstat (limited to 'lib/sqlalchemy/sql')
-rw-r--r--lib/sqlalchemy/sql/base.py88
-rw-r--r--lib/sqlalchemy/sql/compiler.py183
-rw-r--r--lib/sqlalchemy/sql/dml.py20
-rw-r--r--lib/sqlalchemy/sql/elements.py138
-rw-r--r--lib/sqlalchemy/sql/expression.py2
-rw-r--r--lib/sqlalchemy/sql/selectable.py706
-rw-r--r--lib/sqlalchemy/sql/traversals.py5
7 files changed, 596 insertions, 546 deletions
diff --git a/lib/sqlalchemy/sql/base.py b/lib/sqlalchemy/sql/base.py
index 89839ea28..5410f64bb 100644
--- a/lib/sqlalchemy/sql/base.py
+++ b/lib/sqlalchemy/sql/base.py
@@ -47,6 +47,17 @@ class Immutable(object):
pass
+class SingletonConstant(Immutable):
+ def __new__(cls, *arg, **kw):
+ return cls._singleton
+
+ @classmethod
+ def _create_singleton(cls):
+ obj = object.__new__(cls)
+ obj.__init__()
+ cls._singleton = obj
+
+
class HasMemoized(object):
def _reset_memoizations(self):
self._memoized_property.expire_instance(self)
@@ -60,7 +71,19 @@ class HasMemoized(object):
def _from_objects(*elements):
- return itertools.chain(*[element._from_objects for element in elements])
+ return itertools.chain.from_iterable(
+ [element._from_objects for element in elements]
+ )
+
+
+def _select_iterables(elements):
+ """expand tables into individual columns in the
+ given list of column expressions.
+
+ """
+ return itertools.chain.from_iterable(
+ [c._select_iterable for c in elements]
+ )
def _generative(fn):
@@ -420,6 +443,19 @@ class CompileState(object):
__slots__ = ("statement",)
+ @classmethod
+ def _create(cls, statement, compiler, **kw):
+ # factory construction.
+
+ # specific CompileState classes here will look for
+ # "plugins" in the given statement. From there they will invoke
+ # the appropriate plugin constructor if one is found and return
+ # the alternate CompileState object.
+
+ c = cls.__new__(cls)
+ c.__init__(statement, compiler, **kw)
+ return c
+
def __init__(self, statement, compiler, **kw):
self.statement = statement
@@ -433,11 +469,44 @@ class Generative(object):
s.__dict__ = self.__dict__.copy()
return s
+ def options(self, *options):
+ """Apply options to this statement.
+
+ In the general sense, options are any kind of Python object
+ that can be interpreted by the SQL compiler for the statement.
+ These options can be consumed by specific dialects or specific kinds
+ of compilers.
+
+ The most commonly known kind of option are the ORM level options
+ that apply "eager load" and other loading behaviors to an ORM
+ query. However, options can theoretically be used for many other
+ purposes.
+
+ For background on specific kinds of options for specific kinds of
+ statements, refer to the documentation for those option objects.
+
+ .. versionchanged:: 1.4 - added :meth:`.Generative.options` to
+ Core statement objects towards the goal of allowing unified
+ Core / ORM querying capabilities.
+
+ .. seealso::
+
+ :ref:`deferred_options` - refers to options specific to the usage
+ of ORM queries
+
+ :ref:`relationship_loader_options` - refers to options specific
+ to the usage of ORM queries
+
+ """
+ self._options += options
+
class HasCompileState(Generative):
"""A class that has a :class:`.CompileState` associated with it."""
- _compile_state_cls = CompileState
+ _compile_state_factory = CompileState._create
+
+ _compile_state_plugin = None
class Executable(Generative):
@@ -510,6 +579,13 @@ class Executable(Generative):
"""
return self._execution_options
+ @util.deprecated_20(
+ ":meth:`.Executable.execute`",
+ alternative="All statement execution in SQLAlchemy 2.0 is performed "
+ "by the :meth:`.Connection.execute` method of :class:`.Connection`, "
+ "or in the ORM by the :meth:`.Session.execute` method of "
+ ":class:`.Session`.",
+ )
def execute(self, *multiparams, **params):
"""Compile and execute this :class:`.Executable`."""
e = self.bind
@@ -523,6 +599,14 @@ class Executable(Generative):
raise exc.UnboundExecutionError(msg)
return e._execute_clauseelement(self, multiparams, params)
+ @util.deprecated_20(
+ ":meth:`.Executable.scalar`",
+ alternative="All statement execution in SQLAlchemy 2.0 is performed "
+ "by the :meth:`.Connection.execute` method of :class:`.Connection`, "
+ "or in the ORM by the :meth:`.Session.execute` method of "
+ ":class:`.Session`; the :meth:`.Result.scalar` method can then be "
+ "used to return a scalar result.",
+ )
def scalar(self, *multiparams, **params):
"""Compile and execute this :class:`.Executable`, returning the
result's scalar representation.
diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py
index 3ebcf24b0..c39f59e32 100644
--- a/lib/sqlalchemy/sql/compiler.py
+++ b/lib/sqlalchemy/sql/compiler.py
@@ -28,6 +28,7 @@ import contextlib
import itertools
import re
+from . import base
from . import coercions
from . import crud
from . import elements
@@ -1045,9 +1046,13 @@ class SQLCompiler(Compiled):
self, element, within_columns_clause=False, **kwargs
):
if self.stack and self.dialect.supports_simple_order_by_label:
- selectable = self.stack[-1]["selectable"]
+ compile_state = self.stack[-1]["compile_state"]
- with_cols, only_froms, only_cols = selectable._label_resolve_dict
+ (
+ with_cols,
+ only_froms,
+ only_cols,
+ ) = compile_state._label_resolve_dict
if within_columns_clause:
resolve_dict = only_froms
else:
@@ -1082,8 +1087,8 @@ class SQLCompiler(Compiled):
# compiling the element outside of the context of a SELECT
return self.process(element._text_clause)
- selectable = self.stack[-1]["selectable"]
- with_cols, only_froms, only_cols = selectable._label_resolve_dict
+ compile_state = self.stack[-1]["compile_state"]
+ with_cols, only_froms, only_cols = compile_state._label_resolve_dict
try:
if within_columns_clause:
col = only_froms[element.element]
@@ -1313,6 +1318,24 @@ class SQLCompiler(Compiled):
if s
)
+ def _generate_delimited_and_list(self, clauses, **kw):
+
+ lcc, clauses = elements.BooleanClauseList._process_clauses_for_boolean(
+ operators.and_,
+ elements.True_._singleton,
+ elements.False_._singleton,
+ clauses,
+ )
+ if lcc == 1:
+ return clauses[0]._compiler_dispatch(self, **kw)
+ else:
+ separator = OPERATORS[operators.and_]
+ return separator.join(
+ s
+ for s in (c._compiler_dispatch(self, **kw) for c in clauses)
+ if s
+ )
+
def visit_clauselist(self, clauselist, **kw):
sep = clauselist.operator
if sep is None:
@@ -1473,6 +1496,12 @@ class SQLCompiler(Compiled):
self, cs, asfrom=False, compound_index=0, **kwargs
):
toplevel = not self.stack
+
+ compile_state = cs._compile_state_factory(cs, self, **kwargs)
+
+ if toplevel:
+ self.compile_state = compile_state
+
entry = self._default_stack_entry if toplevel else self.stack[-1]
need_result_map = toplevel or (
compound_index == 0
@@ -1484,6 +1513,7 @@ class SQLCompiler(Compiled):
"correlate_froms": entry["correlate_froms"],
"asfrom_froms": entry["asfrom_froms"],
"selectable": cs,
+ "compile_state": compile_state,
"need_result_map_for_compound": need_result_map,
}
)
@@ -1665,7 +1695,6 @@ class SQLCompiler(Compiled):
from_linter=None,
**kw
):
-
if from_linter and operators.is_comparison(binary.operator):
from_linter.edges.update(
itertools.product(
@@ -2273,7 +2302,6 @@ class SQLCompiler(Compiled):
need_column_expressions=False,
):
"""produce labeled columns present in a select()."""
-
impl = column.type.dialect_impl(self.dialect)
if impl._has_column_expression and (
@@ -2349,7 +2377,12 @@ class SQLCompiler(Compiled):
or isinstance(column, functions.FunctionElement)
)
):
- result_expr = _CompileLabel(col_expr, column.anon_label)
+ result_expr = _CompileLabel(
+ col_expr,
+ column.anon_label
+ if not column_is_repeated
+ else column._dedupe_label_anon_label,
+ )
elif col_expr is not column:
# TODO: are we sure "column" has a .name and .key here ?
# assert isinstance(column, elements.ColumnClause)
@@ -2389,7 +2422,9 @@ class SQLCompiler(Compiled):
[("correlate_froms", frozenset()), ("asfrom_froms", frozenset())]
)
- def _display_froms_for_select(self, select, asfrom, lateral=False):
+ def _display_froms_for_select(
+ self, select_stmt, asfrom, lateral=False, **kw
+ ):
# utility method to help external dialects
# get the correct from list for a select.
# specifically the oracle dialect needs this feature
@@ -2397,18 +2432,20 @@ class SQLCompiler(Compiled):
toplevel = not self.stack
entry = self._default_stack_entry if toplevel else self.stack[-1]
+ compile_state = select_stmt._compile_state_factory(select_stmt, self)
+
correlate_froms = entry["correlate_froms"]
asfrom_froms = entry["asfrom_froms"]
if asfrom and not lateral:
- froms = select._get_display_froms(
+ froms = compile_state._get_display_froms(
explicit_correlate_froms=correlate_froms.difference(
asfrom_froms
),
implicit_correlate_froms=(),
)
else:
- froms = select._get_display_froms(
+ froms = compile_state._get_display_froms(
explicit_correlate_froms=correlate_froms,
implicit_correlate_froms=asfrom_froms,
)
@@ -2416,7 +2453,7 @@ class SQLCompiler(Compiled):
def visit_select(
self,
- select,
+ select_stmt,
asfrom=False,
fromhints=None,
compound_index=0,
@@ -2426,7 +2463,16 @@ class SQLCompiler(Compiled):
**kwargs
):
+ compile_state = select_stmt._compile_state_factory(
+ select_stmt, self, **kwargs
+ )
+ select_stmt = compile_state.statement
+
toplevel = not self.stack
+
+ if toplevel:
+ self.compile_state = compile_state
+
entry = self._default_stack_entry if toplevel else self.stack[-1]
populate_result_map = need_column_expressions = (
@@ -2445,7 +2491,7 @@ class SQLCompiler(Compiled):
del kwargs["add_to_result_map"]
froms = self._setup_select_stack(
- select, entry, asfrom, lateral, compound_index
+ select_stmt, compile_state, entry, asfrom, lateral, compound_index
)
column_clause_args = kwargs.copy()
@@ -2455,23 +2501,25 @@ class SQLCompiler(Compiled):
text = "SELECT " # we're off to a good start !
- if select._hints:
- hint_text, byfrom = self._setup_select_hints(select)
+ if select_stmt._hints:
+ hint_text, byfrom = self._setup_select_hints(select_stmt)
if hint_text:
text += hint_text + " "
else:
byfrom = None
- if select._prefixes:
- text += self._generate_prefixes(select, select._prefixes, **kwargs)
+ if select_stmt._prefixes:
+ text += self._generate_prefixes(
+ select_stmt, select_stmt._prefixes, **kwargs
+ )
- text += self.get_select_precolumns(select, **kwargs)
+ text += self.get_select_precolumns(select_stmt, **kwargs)
# the actual list of columns to print in the SELECT column list.
inner_columns = [
c
for c in [
self._label_select_column(
- select,
+ select_stmt,
column,
populate_result_map,
asfrom,
@@ -2480,7 +2528,7 @@ class SQLCompiler(Compiled):
column_is_repeated=repeated,
need_column_expressions=need_column_expressions,
)
- for name, column, repeated in select._columns_plus_names
+ for name, column, repeated in compile_state.columns_plus_names
]
if c is not None
]
@@ -2489,11 +2537,19 @@ class SQLCompiler(Compiled):
# if this select is a compiler-generated wrapper,
# rewrite the targeted columns in the result map
+ compile_state_wraps_for = select_wraps_for._compile_state_factory(
+ select_wraps_for, self, **kwargs
+ )
+
translate = dict(
zip(
[
name
- for (key, name, repeated) in select._columns_plus_names
+ for (
+ key,
+ name,
+ repeated,
+ ) in compile_state.columns_plus_names
],
[
name
@@ -2501,7 +2557,7 @@ class SQLCompiler(Compiled):
key,
name,
repeated,
- ) in select_wraps_for._columns_plus_names
+ ) in compile_state_wraps_for.columns_plus_names
],
)
)
@@ -2512,13 +2568,20 @@ class SQLCompiler(Compiled):
]
text = self._compose_select_body(
- text, select, inner_columns, froms, byfrom, toplevel, kwargs
+ text,
+ select_stmt,
+ compile_state,
+ inner_columns,
+ froms,
+ byfrom,
+ toplevel,
+ kwargs,
)
- if select._statement_hints:
+ if select_stmt._statement_hints:
per_dialect = [
ht
- for (dialect_name, ht) in select._statement_hints
+ for (dialect_name, ht) in select_stmt._statement_hints
if dialect_name in ("*", self.dialect.name)
]
if per_dialect:
@@ -2527,9 +2590,9 @@ class SQLCompiler(Compiled):
if self.ctes and toplevel:
text = self._render_cte_clause() + text
- if select._suffixes:
+ if select_stmt._suffixes:
text += " " + self._generate_prefixes(
- select, select._suffixes, **kwargs
+ select_stmt, select_stmt._suffixes, **kwargs
)
self.stack.pop(-1)
@@ -2552,7 +2615,7 @@ class SQLCompiler(Compiled):
return hint_text, byfrom
def _setup_select_stack(
- self, select, entry, asfrom, lateral, compound_index
+ self, select, compile_state, entry, asfrom, lateral, compound_index
):
correlate_froms = entry["correlate_froms"]
asfrom_froms = entry["asfrom_froms"]
@@ -2563,8 +2626,8 @@ class SQLCompiler(Compiled):
if select_0._is_select_container:
select_0 = select_0.element
numcols = len(select_0.selected_columns)
- # numcols = len(select_0._columns_plus_names)
- if len(select._columns_plus_names) != numcols:
+
+ if len(compile_state.columns_plus_names) != numcols:
raise exc.CompileError(
"All selectables passed to "
"CompoundSelect must have identical numbers of "
@@ -2579,14 +2642,14 @@ class SQLCompiler(Compiled):
)
if asfrom and not lateral:
- froms = select._get_display_froms(
+ froms = compile_state._get_display_froms(
explicit_correlate_froms=correlate_froms.difference(
asfrom_froms
),
implicit_correlate_froms=(),
)
else:
- froms = select._get_display_froms(
+ froms = compile_state._get_display_froms(
explicit_correlate_froms=correlate_froms,
implicit_correlate_froms=asfrom_froms,
)
@@ -2598,13 +2661,22 @@ class SQLCompiler(Compiled):
"asfrom_froms": new_correlate_froms,
"correlate_froms": all_correlate_froms,
"selectable": select,
+ "compile_state": compile_state,
}
self.stack.append(new_entry)
return froms
def _compose_select_body(
- self, text, select, inner_columns, froms, byfrom, toplevel, kwargs
+ self,
+ text,
+ select,
+ compile_state,
+ inner_columns,
+ froms,
+ byfrom,
+ toplevel,
+ kwargs,
):
text += ", ".join(inner_columns)
@@ -2646,9 +2718,9 @@ class SQLCompiler(Compiled):
else:
text += self.default_from()
- if select._whereclause is not None:
- t = select._whereclause._compiler_dispatch(
- self, from_linter=from_linter, **kwargs
+ if select._where_criteria:
+ t = self._generate_delimited_and_list(
+ select._where_criteria, from_linter=from_linter, **kwargs
)
if t:
text += " \nWHERE " + t
@@ -2659,15 +2731,17 @@ class SQLCompiler(Compiled):
):
from_linter.warn()
- if select._group_by_clause.clauses:
+ if select._group_by_clauses:
text += self.group_by_clause(select, **kwargs)
- if select._having is not None:
- t = select._having._compiler_dispatch(self, **kwargs)
+ if select._having_criteria:
+ t = self._generate_delimited_and_list(
+ select._having_criteria, **kwargs
+ )
if t:
text += " \nHAVING " + t
- if select._order_by_clause.clauses:
+ if select._order_by_clauses:
text += self.order_by_clause(select, **kwargs)
if (
@@ -2718,7 +2792,9 @@ class SQLCompiler(Compiled):
def group_by_clause(self, select, **kw):
"""allow dialects to customize how GROUP BY is rendered."""
- group_by = select._group_by_clause._compiler_dispatch(self, **kw)
+ group_by = self._generate_delimited_list(
+ select._group_by_clauses, OPERATORS[operators.comma_op], **kw
+ )
if group_by:
return " GROUP BY " + group_by
else:
@@ -2727,7 +2803,10 @@ class SQLCompiler(Compiled):
def order_by_clause(self, select, **kw):
"""allow dialects to customize how ORDER BY is rendered."""
- order_by = select._order_by_clause._compiler_dispatch(self, **kw)
+ order_by = self._generate_delimited_list(
+ select._order_by_clauses, OPERATORS[operators.comma_op], **kw
+ )
+
if order_by:
return " ORDER BY " + order_by
else:
@@ -2826,8 +2905,8 @@ class SQLCompiler(Compiled):
def visit_insert(self, insert_stmt, **kw):
- compile_state = insert_stmt._compile_state_cls(
- insert_stmt, self, isinsert=True, **kw
+ compile_state = insert_stmt._compile_state_factory(
+ insert_stmt, self, **kw
)
insert_stmt = compile_state.statement
@@ -2972,8 +3051,8 @@ class SQLCompiler(Compiled):
)
def visit_update(self, update_stmt, **kw):
- compile_state = update_stmt._compile_state_cls(
- update_stmt, self, isupdate=True, **kw
+ compile_state = update_stmt._compile_state_factory(
+ update_stmt, self, **kw
)
update_stmt = compile_state.statement
@@ -3055,8 +3134,8 @@ class SQLCompiler(Compiled):
text += " " + extra_from_text
if update_stmt._where_criteria:
- t = self._generate_delimited_list(
- update_stmt._where_criteria, OPERATORS[operators.and_], **kw
+ t = self._generate_delimited_and_list(
+ update_stmt._where_criteria, **kw
)
if t:
text += " WHERE " + t
@@ -3099,8 +3178,8 @@ class SQLCompiler(Compiled):
return from_table._compiler_dispatch(self, asfrom=True, iscrud=True)
def visit_delete(self, delete_stmt, **kw):
- compile_state = delete_stmt._compile_state_cls(
- delete_stmt, self, isdelete=True, **kw
+ compile_state = delete_stmt._compile_state_factory(
+ delete_stmt, self, **kw
)
delete_stmt = compile_state.statement
@@ -3158,8 +3237,8 @@ class SQLCompiler(Compiled):
text += " " + extra_from_text
if delete_stmt._where_criteria:
- t = self._generate_delimited_list(
- delete_stmt._where_criteria, OPERATORS[operators.and_], **kw
+ t = self._generate_delimited_and_list(
+ delete_stmt._where_criteria, **kw
)
if t:
text += " WHERE " + t
@@ -3229,7 +3308,7 @@ class StrSQLCompiler(SQLCompiler):
def returning_clause(self, stmt, returning_cols):
columns = [
self._label_select_column(None, c, True, False, {})
- for c in elements._select_iterables(returning_cols)
+ for c in base._select_iterables(returning_cols)
]
return "RETURNING " + ", ".join(columns)
diff --git a/lib/sqlalchemy/sql/dml.py b/lib/sqlalchemy/sql/dml.py
index 171a2cc2c..2349bfd03 100644
--- a/lib/sqlalchemy/sql/dml.py
+++ b/lib/sqlalchemy/sql/dml.py
@@ -37,6 +37,18 @@ class DMLState(CompileState):
isdelete = False
isinsert = False
+ @classmethod
+ def _create_insert(cls, statement, compiler, **kw):
+ return DMLState(statement, compiler, isinsert=True, **kw)
+
+ @classmethod
+ def _create_update(cls, statement, compiler, **kw):
+ return DMLState(statement, compiler, isupdate=True, **kw)
+
+ @classmethod
+ def _create_delete(cls, statement, compiler, **kw):
+ return DMLState(statement, compiler, isdelete=True, **kw)
+
def __init__(
self,
statement,
@@ -181,8 +193,6 @@ class UpdateBase(
_hints = util.immutabledict()
named_with_column = False
- _compile_state_cls = DMLState
-
@classmethod
def _constructor_20_deprecations(cls, fn_name, clsname, names):
@@ -717,6 +727,8 @@ class Insert(ValuesBase):
_supports_multi_parameters = True
+ _compile_state_factory = DMLState._create_insert
+
select = None
include_insert_from_select_defaults = False
@@ -915,6 +927,8 @@ class Update(DMLWhereBase, ValuesBase):
__visit_name__ = "update"
+ _compile_state_factory = DMLState._create_update
+
_traverse_internals = (
[
("table", InternalTraversal.dp_clauseelement),
@@ -1153,6 +1167,8 @@ class Delete(DMLWhereBase, UpdateBase):
__visit_name__ = "delete"
+ _compile_state_factory = DMLState._create_delete
+
_traverse_internals = (
[
("table", InternalTraversal.dp_clauseelement),
diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py
index 47739a37d..f9abfecd6 100644
--- a/lib/sqlalchemy/sql/elements.py
+++ b/lib/sqlalchemy/sql/elements.py
@@ -31,6 +31,7 @@ from .base import HasMemoized
from .base import Immutable
from .base import NO_ARG
from .base import PARSE_AUTOCOMMIT
+from .base import SingletonConstant
from .coercions import _document_text_coercion
from .traversals import _copy_internals
from .traversals import _get_children
@@ -338,7 +339,7 @@ class ClauseElement(
"""
return traversals.compare(self, other, **kw)
- def _copy_internals(self, **kw):
+ def _copy_internals(self, omit_attrs=(), **kw):
"""Reassign internal elements to be clones of themselves.
Called during a copy-and-traverse operation on newly
@@ -358,12 +359,15 @@ class ClauseElement(
for attrname, obj, meth in _copy_internals.run_generated_dispatch(
self, traverse_internals, "_generated_copy_internals_traversal"
):
+ if attrname in omit_attrs:
+ continue
+
if obj is not None:
result = meth(self, obj, **kw)
if result is not None:
setattr(self, attrname, result)
- def get_children(self, omit_attrs=None, **kw):
+ def get_children(self, omit_attrs=(), **kw):
r"""Return immediate child :class:`.Traversible` elements of this
:class:`.Traversible`.
@@ -385,7 +389,7 @@ class ClauseElement(
for attrname, obj, meth in _get_children.run_generated_dispatch(
self, traverse_internals, "_generated_get_children_traversal"
):
- if obj is None or omit_attrs and attrname in omit_attrs:
+ if obj is None or attrname in omit_attrs:
continue
result.extend(meth(obj, **kw))
return result
@@ -941,7 +945,8 @@ class ColumnElement(
@util.memoized_property
def _dedupe_label_anon_label(self):
- return self._anon_label(getattr(self, "_label", "anon") + "_")
+ label = getattr(self, "_label", None) or "anon"
+ return self._anon_label(label + "_")
class WrapsColumnExpression(object):
@@ -1481,6 +1486,8 @@ class TextClause(
)
_is_implicitly_boolean = False
+ _render_label_in_columns_clause = False
+
def __and__(self, other):
# support use in select.where(), query.filter()
return and_(self, other)
@@ -1513,14 +1520,6 @@ class TextClause(
@classmethod
@util.deprecated_params(
- autocommit=(
- "0.6",
- "The :paramref:`.text.autocommit` parameter is deprecated and "
- "will be removed in a future release. Please use the "
- ":paramref:`.Connection.execution_options.autocommit` parameter "
- "in conjunction with the :meth:`.Executable.execution_options` "
- "method.",
- ),
bindparams=(
"0.9",
"The :paramref:`.text.bindparams` parameter "
@@ -1536,7 +1535,7 @@ class TextClause(
)
@_document_text_coercion("text", ":func:`.text`", ":paramref:`.text.text`")
def _create_text(
- self, text, bind=None, bindparams=None, typemap=None, autocommit=None
+ self, text, bind=None, bindparams=None, typemap=None,
):
r"""Construct a new :class:`.TextClause` clause, representing
a textual SQL string directly.
@@ -1613,9 +1612,6 @@ class TextClause(
to specify bind parameters; they will be compiled to their
engine-specific format.
- :param autocommit: whether or not to set the "autocommit" execution
- option for this :class:`.TextClause` object.
-
:param bind:
an optional connection or engine to be used for this text query.
@@ -1650,8 +1646,6 @@ class TextClause(
stmt = stmt.bindparams(*bindparams)
if typemap:
stmt = stmt.columns(**typemap)
- if autocommit is not None:
- stmt = stmt.execution_options(autocommit=autocommit)
return stmt
@@ -1920,7 +1914,7 @@ class TextClause(
return self
-class Null(roles.ConstExprRole, ColumnElement):
+class Null(SingletonConstant, roles.ConstExprRole, ColumnElement):
"""Represent the NULL keyword in a SQL statement.
:class:`.Null` is accessed as a constant via the
@@ -1943,7 +1937,10 @@ class Null(roles.ConstExprRole, ColumnElement):
return Null()
-class False_(roles.ConstExprRole, ColumnElement):
+Null._create_singleton()
+
+
+class False_(SingletonConstant, roles.ConstExprRole, ColumnElement):
"""Represent the ``false`` keyword, or equivalent, in a SQL statement.
:class:`.False_` is accessed as a constant via the
@@ -2000,7 +1997,10 @@ class False_(roles.ConstExprRole, ColumnElement):
return False_()
-class True_(roles.ConstExprRole, ColumnElement):
+False_._create_singleton()
+
+
+class True_(SingletonConstant, roles.ConstExprRole, ColumnElement):
"""Represent the ``true`` keyword, or equivalent, in a SQL statement.
:class:`.True_` is accessed as a constant via the
@@ -2065,6 +2065,9 @@ class True_(roles.ConstExprRole, ColumnElement):
return True_()
+True_._create_singleton()
+
+
class ClauseList(
roles.InElementRole,
roles.OrderByRole,
@@ -2106,6 +2109,17 @@ class ClauseList(
]
self._is_implicitly_boolean = operators.is_boolean(self.operator)
+ @classmethod
+ def _construct_raw(cls, operator, clauses=None):
+ self = cls.__new__(cls)
+ self.clauses = clauses if clauses else []
+ self.group = True
+ self.operator = operator
+ self.group_contents = True
+ self._tuple_values = False
+ self._is_implicitly_boolean = False
+ return self
+
def __iter__(self):
return iter(self.clauses)
@@ -2151,46 +2165,58 @@ class BooleanClauseList(ClauseList, ColumnElement):
)
@classmethod
- def _construct(cls, operator, continue_on, skip_on, *clauses, **kw):
-
+ def _process_clauses_for_boolean(
+ cls, operator, continue_on, skip_on, clauses
+ ):
has_continue_on = None
- special_elements = (continue_on, skip_on)
- convert_clauses = []
-
- for clause in util.coerce_generator_arg(clauses):
- clause = coercions.expect(roles.WhereHavingRole, clause)
- # elements that are not the continue/skip are the most
- # common, try to have only one isinstance() call for that case.
- if not isinstance(clause, special_elements):
- convert_clauses.append(clause)
- elif isinstance(clause, skip_on):
- # instance of skip_on, e.g. and_(x, y, False, z), cancels
- # the rest out
- return clause.self_group(against=operators._asbool)
- elif has_continue_on is None:
+ convert_clauses = []
+ for clause in clauses:
+ if clause is continue_on:
# instance of continue_on, like and_(x, y, True, z), store it
# if we didn't find one already, we will use it if there
# are no other expressions here.
has_continue_on = clause
+ elif clause is skip_on:
+ # instance of skip_on, e.g. and_(x, y, False, z), cancels
+ # the rest out
+ convert_clauses = [clause]
+ break
+ else:
+ convert_clauses.append(clause)
+
+ if not convert_clauses and has_continue_on is not None:
+ convert_clauses = [has_continue_on]
lcc = len(convert_clauses)
if lcc > 1:
+ against = operator
+ else:
+ against = operators._asbool
+ return lcc, [c.self_group(against=against) for c in convert_clauses]
+
+ @classmethod
+ def _construct(cls, operator, continue_on, skip_on, *clauses, **kw):
+
+ lcc, convert_clauses = cls._process_clauses_for_boolean(
+ operator,
+ continue_on,
+ skip_on,
+ [
+ coercions.expect(roles.WhereHavingRole, clause)
+ for clause in util.coerce_generator_arg(clauses)
+ ],
+ )
+
+ if lcc > 1:
# multiple elements. Return regular BooleanClauseList
# which will link elements against the operator.
- return cls._construct_raw(
- operator,
- [c.self_group(against=operator) for c in convert_clauses],
- )
+ return cls._construct_raw(operator, convert_clauses)
elif lcc == 1:
# just one element. return it as a single boolean element,
# not a list and discard the operator.
- return convert_clauses[0].self_group(against=operators._asbool)
- elif not lcc and has_continue_on is not None:
- # no elements but we had a "continue", just return the continue
- # as a boolean element, discard the operator.
- return has_continue_on.self_group(against=operators._asbool)
+ return convert_clauses[0]
else:
# no elements period. deprecated use case. return an empty
# ClauseList construct that generates nothing unless it has
@@ -2201,7 +2227,9 @@ class BooleanClauseList(ClauseList, ColumnElement):
"%(name)s() construct, use %(name)s(%(continue_on)s, *args)."
% {
"name": operator.__name__,
- "continue_on": "True" if continue_on is True_ else "False",
+ "continue_on": "True"
+ if continue_on is True_._singleton
+ else "False",
}
)
return cls._construct_raw(operator)
@@ -2275,7 +2303,9 @@ class BooleanClauseList(ClauseList, ColumnElement):
:func:`.or_`
"""
- return cls._construct(operators.and_, True_, False_, *clauses)
+ return cls._construct(
+ operators.and_, True_._singleton, False_._singleton, *clauses
+ )
@classmethod
def or_(cls, *clauses):
@@ -2326,7 +2356,9 @@ class BooleanClauseList(ClauseList, ColumnElement):
:func:`.and_`
"""
- return cls._construct(operators.or_, False_, True_, *clauses)
+ return cls._construct(
+ operators.or_, False_._singleton, True_._singleton, *clauses
+ )
@property
def _select_iterable(self):
@@ -4530,14 +4562,6 @@ class quoted_name(util.MemoizedSlots, util.text_type):
return str.__repr__(self)
-def _select_iterables(elements):
- """expand tables into individual columns in the
- given list of column expressions.
-
- """
- return itertools.chain(*[c._select_iterable for c in elements])
-
-
def _find_columns(clause):
"""locate Column objects within the given expression."""
diff --git a/lib/sqlalchemy/sql/expression.py b/lib/sqlalchemy/sql/expression.py
index 7ea0e2ad3..780648df0 100644
--- a/lib/sqlalchemy/sql/expression.py
+++ b/lib/sqlalchemy/sql/expression.py
@@ -84,6 +84,7 @@ __all__ = [
from .base import _from_objects # noqa
+from .base import _select_iterables # noqa
from .base import ColumnCollection # noqa
from .base import Executable # noqa
from .base import PARSE_AUTOCOMMIT # noqa
@@ -92,7 +93,6 @@ from .dml import Insert # noqa
from .dml import Update # noqa
from .dml import UpdateBase # noqa
from .dml import ValuesBase # noqa
-from .elements import _select_iterables # noqa
from .elements import _truncated_label # noqa
from .elements import between # noqa
from .elements import BinaryExpression # noqa
diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py
index 965ac6e7f..ae11f680c 100644
--- a/lib/sqlalchemy/sql/selectable.py
+++ b/lib/sqlalchemy/sql/selectable.py
@@ -26,16 +26,18 @@ from .base import _cloned_intersection
from .base import _expand_cloned
from .base import _from_objects
from .base import _generative
+from .base import _select_iterables
from .base import ColumnCollection
from .base import ColumnSet
+from .base import CompileState
from .base import DedupeColumnCollection
from .base import Executable
from .base import Generative
+from .base import HasCompileState
from .base import HasMemoized
from .base import Immutable
from .coercions import _document_text_coercion
from .elements import _anonymous_label
-from .elements import _select_iterables
from .elements import and_
from .elements import BindParameter
from .elements import ClauseElement
@@ -44,7 +46,6 @@ from .elements import ColumnClause
from .elements import GroupedElement
from .elements import Grouping
from .elements import literal_column
-from .elements import True_
from .elements import UnaryExpression
from .visitors import InternalTraversal
from .. import exc
@@ -1995,54 +1996,6 @@ class ForUpdateArg(ClauseElement):
("skip_locked", InternalTraversal.dp_boolean),
]
- @classmethod
- def parse_legacy_select(self, arg):
- """Parse the for_update argument of :func:`.select`.
-
- :param mode: Defines the lockmode to use.
-
- ``None`` - translates to no lockmode
-
- ``'update'`` - translates to ``FOR UPDATE``
- (standard SQL, supported by most dialects)
-
- ``'nowait'`` - translates to ``FOR UPDATE NOWAIT``
- (supported by Oracle, PostgreSQL 8.1 upwards)
-
- ``'read'`` - translates to ``LOCK IN SHARE MODE`` (for MySQL),
- and ``FOR SHARE`` (for PostgreSQL)
-
- ``'read_nowait'`` - translates to ``FOR SHARE NOWAIT``
- (supported by PostgreSQL). ``FOR SHARE`` and
- ``FOR SHARE NOWAIT`` (PostgreSQL).
-
- """
- if arg in (None, False):
- return None
-
- nowait = read = False
- if arg == "nowait":
- nowait = True
- elif arg == "read":
- read = True
- elif arg == "read_nowait":
- read = nowait = True
- elif arg is not True:
- raise exc.ArgumentError("Unknown for_update argument: %r" % arg)
-
- return ForUpdateArg(read=read, nowait=nowait)
-
- @property
- def legacy_for_update_value(self):
- if self.read and not self.nowait:
- return "read"
- elif self.read and self.nowait:
- return "read_nowait"
- elif self.nowait:
- return "nowait"
- else:
- return True
-
def __eq__(self, other):
return (
isinstance(other, ForUpdateArg)
@@ -2262,24 +2215,6 @@ class SelectBase(
"""
return Lateral._factory(self, name)
- @_generative
- @util.deprecated(
- "0.6",
- message="The :meth:`.SelectBase.autocommit` method is deprecated, "
- "and will be removed in a future release. Please use the "
- "the :paramref:`.Connection.execution_options.autocommit` "
- "parameter in conjunction with the "
- ":meth:`.Executable.execution_options` method.",
- )
- def autocommit(self):
- """return a new selectable with the 'autocommit' flag set to
- True.
- """
-
- self._execution_options = self._execution_options.union(
- {"autocommit": True}
- )
-
def _generate(self):
"""Override the default _generate() method to also clear out
exported collections."""
@@ -2459,8 +2394,8 @@ class GenerativeSelect(DeprecatedSelectBaseGenerations, SelectBase):
"""
- _order_by_clause = ClauseList()
- _group_by_clause = ClauseList()
+ _order_by_clauses = ()
+ _group_by_clauses = ()
_limit_clause = None
_offset_clause = None
_for_update_arg = None
@@ -2468,56 +2403,25 @@ class GenerativeSelect(DeprecatedSelectBaseGenerations, SelectBase):
def __init__(
self,
use_labels=False,
- for_update=False,
limit=None,
offset=None,
order_by=None,
group_by=None,
bind=None,
- autocommit=None,
):
self.use_labels = use_labels
- if for_update is not False:
- self._for_update_arg = ForUpdateArg.parse_legacy_select(for_update)
-
- if autocommit is not None:
- util.warn_deprecated(
- "The select.autocommit parameter is deprecated and will be "
- "removed in a future release. Please refer to the "
- "Select.execution_options.autocommit` parameter."
- )
- self._execution_options = self._execution_options.union(
- {"autocommit": autocommit}
- )
if limit is not None:
- self._limit_clause = self._offset_or_limit_clause(limit)
+ self.limit.non_generative(self, limit)
if offset is not None:
- self._offset_clause = self._offset_or_limit_clause(offset)
- self._bind = bind
+ self.offset.non_generative(self, offset)
if order_by is not None:
- self._order_by_clause = ClauseList(
- *util.to_list(order_by),
- _literal_as_text_role=roles.OrderByRole
- )
+ self.order_by.non_generative(self, *util.to_list(order_by))
if group_by is not None:
- self._group_by_clause = ClauseList(
- *util.to_list(group_by), _literal_as_text_role=roles.ByOfRole
- )
+ self.group_by.non_generative(self, *util.to_list(group_by))
- @property
- def for_update(self):
- """Provide legacy dialect support for the ``for_update`` attribute.
- """
- if self._for_update_arg is not None:
- return self._for_update_arg.legacy_for_update_value
- else:
- return None
-
- @for_update.setter
- def for_update(self, value):
- self._for_update_arg = ForUpdateArg.parse_legacy_select(value)
+ self._bind = bind
@_generative
def with_for_update(
@@ -2566,14 +2470,10 @@ class GenerativeSelect(DeprecatedSelectBaseGenerations, SelectBase):
on Oracle and PostgreSQL dialects or ``FOR SHARE SKIP LOCKED`` if
``read=True`` is also specified.
- .. versionadded:: 1.1.0
-
:param key_share: boolean, will render ``FOR NO KEY UPDATE``,
or if combined with ``read=True`` will render ``FOR KEY SHARE``,
on the PostgreSQL dialect.
- .. versionadded:: 1.1.0
-
"""
self._for_update_arg = ForUpdateArg(
nowait=nowait,
@@ -2597,6 +2497,20 @@ class GenerativeSelect(DeprecatedSelectBaseGenerations, SelectBase):
"""
self.use_labels = True
+ @property
+ def _group_by_clause(self):
+ """ClauseList access to group_by_clauses for legacy dialects"""
+ return ClauseList._construct_raw(
+ operators.comma_op, self._group_by_clauses
+ )
+
+ @property
+ def _order_by_clause(self):
+ """ClauseList access to order_by_clauses for legacy dialects"""
+ return ClauseList._construct_raw(
+ operators.comma_op, self._order_by_clauses
+ )
+
def _offset_or_limit_clause(self, element, name=None, type_=None):
"""Convert the given value to an "offset or limit" clause.
@@ -2728,12 +2642,11 @@ class GenerativeSelect(DeprecatedSelectBaseGenerations, SelectBase):
"""
if len(clauses) == 1 and clauses[0] is None:
- self._order_by_clause = ClauseList()
+ self._order_by_clauses = ()
else:
- if getattr(self, "_order_by_clause", None) is not None:
- clauses = list(self._order_by_clause) + list(clauses)
- self._order_by_clause = ClauseList(
- *clauses, _literal_as_text_role=roles.OrderByRole
+ self._order_by_clauses += tuple(
+ coercions.expect(roles.OrderByRole, clause)
+ for clause in clauses
)
@_generative
@@ -2756,20 +2669,24 @@ class GenerativeSelect(DeprecatedSelectBaseGenerations, SelectBase):
"""
if len(clauses) == 1 and clauses[0] is None:
- self._group_by_clause = ClauseList()
+ self._group_by_clauses = ()
else:
- if getattr(self, "_group_by_clause", None) is not None:
- clauses = list(self._group_by_clause) + list(clauses)
- self._group_by_clause = ClauseList(
- *clauses, _literal_as_text_role=roles.ByOfRole
+ self._group_by_clauses += tuple(
+ coercions.expect(roles.ByOfRole, clause) for clause in clauses
)
- @property
+
+class CompoundSelectState(CompileState):
+ @util.memoized_property
def _label_resolve_dict(self):
- raise NotImplementedError()
+ # TODO: this is hacky and slow
+ hacky_subquery = self.statement.subquery()
+ hacky_subquery.named_with_column = False
+ d = dict((c.key, c) for c in hacky_subquery.c)
+ return d, d, d
-class CompoundSelect(GenerativeSelect):
+class CompoundSelect(HasCompileState, GenerativeSelect):
"""Forms the basis of ``UNION``, ``UNION ALL``, and other
SELECT-based set operations.
@@ -2791,13 +2708,14 @@ class CompoundSelect(GenerativeSelect):
"""
__visit_name__ = "compound_select"
+ _compile_state_factory = CompoundSelectState._create
_traverse_internals = [
("selects", InternalTraversal.dp_clauseelement_list),
("_limit_clause", InternalTraversal.dp_clauseelement),
("_offset_clause", InternalTraversal.dp_clauseelement),
- ("_order_by_clause", InternalTraversal.dp_clauseelement),
- ("_group_by_clause", InternalTraversal.dp_clauseelement),
+ ("_order_by_clauses", InternalTraversal.dp_clauseelement_list),
+ ("_group_by_clauses", InternalTraversal.dp_clauseelement_list),
("_for_update_arg", InternalTraversal.dp_clauseelement),
("keyword", InternalTraversal.dp_string),
] + SupportsCloneAnnotations._clone_annotations_traverse_internals
@@ -2823,14 +2741,6 @@ class CompoundSelect(GenerativeSelect):
GenerativeSelect.__init__(self, **kwargs)
- @SelectBase._memoized_property
- def _label_resolve_dict(self):
- # TODO: this is hacky and slow
- hacky_subquery = self.subquery()
- hacky_subquery.named_with_column = False
- d = dict((c.key, c) for c in hacky_subquery.c)
- return d, d, d
-
@classmethod
def _create_union(cls, *selects, **kwargs):
r"""Return a ``UNION`` of multiple selectables.
@@ -3002,6 +2912,7 @@ class CompoundSelect(GenerativeSelect):
"""
return self.selects[0].selected_columns
+ @property
def bind(self):
if self._bind:
return self._bind
@@ -3012,11 +2923,10 @@ class CompoundSelect(GenerativeSelect):
else:
return None
- def _set_bind(self, bind):
+ @bind.setter
+ def bind(self, bind):
self._bind = bind
- bind = property(bind, _set_bind)
-
class DeprecatedSelectGenerations(object):
@util.deprecated(
@@ -3136,8 +3046,130 @@ class DeprecatedSelectGenerations(object):
self.select_from.non_generative(self, fromclause)
+class SelectState(CompileState):
+ def __init__(self, statement, compiler, **kw):
+ self.statement = statement
+ self.froms = self._get_froms(statement)
+
+ self.columns_plus_names = statement._generate_columns_plus_names(True)
+
+ def _get_froms(self, statement):
+ froms = []
+ seen = set()
+
+ for item in statement._iterate_from_elements():
+ if item._is_subquery and item.element is statement:
+ raise exc.InvalidRequestError(
+ "select() construct refers to itself as a FROM"
+ )
+ if not seen.intersection(item._cloned_set):
+ froms.append(item)
+ seen.update(item._cloned_set)
+
+ return froms
+
+ def _get_display_froms(
+ self, explicit_correlate_froms=None, implicit_correlate_froms=None
+ ):
+ """Return the full list of 'from' clauses to be displayed.
+
+ Takes into account a set of existing froms which may be
+ rendered in the FROM clause of enclosing selects; this Select
+ may want to leave those absent if it is automatically
+ correlating.
+
+ """
+ froms = self.froms
+
+ toremove = set(
+ itertools.chain.from_iterable(
+ [_expand_cloned(f._hide_froms) for f in froms]
+ )
+ )
+ if toremove:
+ # filter out to FROM clauses not in the list,
+ # using a list to maintain ordering
+ froms = [f for f in froms if f not in toremove]
+
+ if self.statement._correlate:
+ to_correlate = self.statement._correlate
+ if to_correlate:
+ froms = [
+ f
+ for f in froms
+ if f
+ not in _cloned_intersection(
+ _cloned_intersection(
+ froms, explicit_correlate_froms or ()
+ ),
+ to_correlate,
+ )
+ ]
+
+ if self.statement._correlate_except is not None:
+
+ froms = [
+ f
+ for f in froms
+ if f
+ not in _cloned_difference(
+ _cloned_intersection(
+ froms, explicit_correlate_froms or ()
+ ),
+ self.statement._correlate_except,
+ )
+ ]
+
+ if (
+ self.statement._auto_correlate
+ and implicit_correlate_froms
+ and len(froms) > 1
+ ):
+
+ froms = [
+ f
+ for f in froms
+ if f
+ not in _cloned_intersection(froms, implicit_correlate_froms)
+ ]
+
+ if not len(froms):
+ raise exc.InvalidRequestError(
+ "Select statement '%s"
+ "' returned no FROM clauses "
+ "due to auto-correlation; "
+ "specify correlate(<tables>) "
+ "to control correlation "
+ "manually." % self
+ )
+
+ return froms
+
+ @util.memoized_property
+ def _label_resolve_dict(self):
+ with_cols = dict(
+ (c._resolve_label or c._label or c.key, c)
+ for c in _select_iterables(self.statement._raw_columns)
+ if c._allow_label_resolve
+ )
+ only_froms = dict(
+ (c.key, c)
+ for c in _select_iterables(self.froms)
+ if c._allow_label_resolve
+ )
+ only_cols = with_cols.copy()
+ for key, value in only_froms.items():
+ with_cols.setdefault(key, value)
+
+ return with_cols, only_froms, only_cols
+
+
class Select(
- HasPrefixes, HasSuffixes, DeprecatedSelectGenerations, GenerativeSelect
+ HasPrefixes,
+ HasSuffixes,
+ HasCompileState,
+ DeprecatedSelectGenerations,
+ GenerativeSelect,
):
"""Represents a ``SELECT`` statement.
@@ -3145,28 +3177,29 @@ class Select(
__visit_name__ = "select"
+ _compile_state_factory = SelectState._create
+
_hints = util.immutabledict()
_statement_hints = ()
_distinct = False
_distinct_on = ()
_correlate = ()
_correlate_except = None
+ _where_criteria = ()
+ _having_criteria = ()
+ _from_obj = ()
+ _auto_correlate = True
+
_memoized_property = SelectBase._memoized_property
_traverse_internals = (
[
- ("_from_obj", InternalTraversal.dp_fromclause_ordered_set),
+ ("_from_obj", InternalTraversal.dp_clauseelement_list),
("_raw_columns", InternalTraversal.dp_clauseelement_list),
- ("_whereclause", InternalTraversal.dp_clauseelement),
- ("_having", InternalTraversal.dp_clauseelement),
- (
- "_order_by_clause.clauses",
- InternalTraversal.dp_clauseelement_list,
- ),
- (
- "_group_by_clause.clauses",
- InternalTraversal.dp_clauseelement_list,
- ),
+ ("_where_criteria", InternalTraversal.dp_clauseelement_list),
+ ("_having_criteria", InternalTraversal.dp_clauseelement_list),
+ ("_order_by_clauses", InternalTraversal.dp_clauseelement_list,),
+ ("_group_by_clauses", InternalTraversal.dp_clauseelement_list,),
("_correlate", InternalTraversal.dp_clauseelement_unordered_set),
(
"_correlate_except",
@@ -3221,13 +3254,6 @@ class Select(
for ent in util.to_list(entities)
]
- # this should all go away once Select is converted to have
- # default state at the class level
- self._auto_correlate = True
- self._from_obj = util.OrderedSet()
- self._whereclause = None
- self._having = None
-
GenerativeSelect.__init__(self)
return self
@@ -3245,24 +3271,6 @@ class Select(
else:
return Select._create_select(*entities)
- @util.deprecated_params(
- autocommit=(
- "0.6",
- "The :paramref:`.select.autocommit` parameter is deprecated "
- "and will be removed in a future release. Please refer to "
- "the :paramref:`.Connection.execution_options.autocommit` "
- "parameter in conjunction with the the "
- ":meth:`.Executable.execution_options` method in order to "
- "affect the autocommit behavior for a statement.",
- ),
- for_update=(
- "0.9",
- "The :paramref:`.select.for_update` parameter is deprecated and "
- "will be removed in a future release. Please refer to the "
- ":meth:`.Select.with_for_update` to specify the "
- "structure of the ``FOR UPDATE`` clause.",
- ),
- )
def __init__(
self,
columns=None,
@@ -3333,8 +3341,6 @@ class Select(
:meth:`.Select.select_from` - full description of explicit
FROM clause specification.
- :param autocommit: legacy autocommit parameter.
-
:param bind=None:
an :class:`~.Engine` or :class:`~.Connection` instance
to which the
@@ -3370,25 +3376,6 @@ class Select(
:meth:`.Select.distinct`
- :param for_update=False:
- when ``True``, applies ``FOR UPDATE`` to the end of the
- resulting statement.
-
- ``for_update`` accepts various string values interpreted by
- specific backends, including:
-
- * ``"read"`` - on MySQL, translates to ``LOCK IN SHARE MODE``;
- on PostgreSQL, translates to ``FOR SHARE``.
- * ``"nowait"`` - on PostgreSQL and Oracle, translates to
- ``FOR UPDATE NOWAIT``.
- * ``"read_nowait"`` - on PostgreSQL, translates to
- ``FOR SHARE NOWAIT``.
-
- .. seealso::
-
- :meth:`.Select.with_for_update` - improved API for
- specifying the ``FOR UPDATE`` clause.
-
:param group_by:
a list of :class:`.ClauseElement` objects which will comprise the
``GROUP BY`` clause of the resulting select. This parameter
@@ -3470,23 +3457,15 @@ class Select(
)
self._auto_correlate = correlate
+
if distinct is not False:
- self._distinct = True
- if not isinstance(distinct, bool):
- self._distinct_on = tuple(
- [
- coercions.expect(roles.ByOfRole, e)
- for e in util.to_list(distinct)
- ]
- )
+ if distinct is True:
+ self.distinct.non_generative(self)
+ else:
+ self.distinct.non_generative(self, *util.to_list(distinct))
if from_obj is not None:
- self._from_obj = util.OrderedSet(
- coercions.expect(roles.FromClauseRole, f)
- for f in util.to_list(from_obj)
- )
- else:
- self._from_obj = util.OrderedSet()
+ self.select_from.non_generative(self, *util.to_list(from_obj))
try:
cols_present = bool(columns)
@@ -3500,26 +3479,17 @@ class Select(
)
if cols_present:
- self._raw_columns = []
- for c in columns:
- c = coercions.expect(roles.ColumnsClauseRole, c)
- self._raw_columns.append(c)
+ self._raw_columns = [
+ coercions.expect(roles.ColumnsClauseRole, c,) for c in columns
+ ]
else:
self._raw_columns = []
if whereclause is not None:
- self._whereclause = coercions.expect(
- roles.WhereHavingRole, whereclause
- ).self_group(against=operators._asbool)
- else:
- self._whereclause = None
+ self.where.non_generative(self, whereclause)
if having is not None:
- self._having = coercions.expect(
- roles.WhereHavingRole, having
- ).self_group(against=operators._asbool)
- else:
- self._having = None
+ self.having.non_generative(self, having)
if prefixes:
self._setup_prefixes(prefixes)
@@ -3529,119 +3499,27 @@ class Select(
GenerativeSelect.__init__(self, **kwargs)
- @property
- def _froms(self):
- # current roadblock to caching is two tests that test that the
- # SELECT can be compiled to a string, then a Table is created against
- # columns, then it can be compiled again and works. this is somewhat
- # valid as people make select() against declarative class where
- # columns don't have their Table yet and perhaps some operations
- # call upon _froms and cache it too soon.
- froms = []
- seen = set()
-
- for item in itertools.chain(
- _from_objects(*self._raw_columns),
- _from_objects(self._whereclause)
- if self._whereclause is not None
- else (),
- self._from_obj,
- ):
- if item._is_subquery and item.element is self:
- raise exc.InvalidRequestError(
- "select() construct refers to itself as a FROM"
- )
- if not seen.intersection(item._cloned_set):
- froms.append(item)
- seen.update(item._cloned_set)
-
- return froms
-
- def _get_display_froms(
- self, explicit_correlate_froms=None, implicit_correlate_froms=None
- ):
- """Return the full list of 'from' clauses to be displayed.
-
- Takes into account a set of existing froms which may be
- rendered in the FROM clause of enclosing selects; this Select
- may want to leave those absent if it is automatically
- correlating.
-
- """
- froms = self._froms
-
- toremove = set(
- itertools.chain(*[_expand_cloned(f._hide_froms) for f in froms])
- )
- if toremove:
- # filter out to FROM clauses not in the list,
- # using a list to maintain ordering
- froms = [f for f in froms if f not in toremove]
-
- if self._correlate:
- to_correlate = self._correlate
- if to_correlate:
- froms = [
- f
- for f in froms
- if f
- not in _cloned_intersection(
- _cloned_intersection(
- froms, explicit_correlate_froms or ()
- ),
- to_correlate,
- )
- ]
-
- if self._correlate_except is not None:
-
- froms = [
- f
- for f in froms
- if f
- not in _cloned_difference(
- _cloned_intersection(
- froms, explicit_correlate_froms or ()
- ),
- self._correlate_except,
- )
- ]
-
- if (
- self._auto_correlate
- and implicit_correlate_froms
- and len(froms) > 1
- ):
-
- froms = [
- f
- for f in froms
- if f
- not in _cloned_intersection(froms, implicit_correlate_froms)
- ]
-
- if not len(froms):
- raise exc.InvalidRequestError(
- "Select statement '%s"
- "' returned no FROM clauses "
- "due to auto-correlation; "
- "specify correlate(<tables>) "
- "to control correlation "
- "manually." % self
- )
-
- return froms
-
def _scalar_type(self):
elem = self._raw_columns[0]
cols = list(elem._select_iterable)
return cols[0].type
+ def _iterate_from_elements(self):
+ return itertools.chain(
+ itertools.chain.from_iterable(
+ [element._from_objects for element in self._raw_columns]
+ ),
+ itertools.chain.from_iterable(
+ [element._from_objects for element in self._where_criteria]
+ ),
+ self._from_obj,
+ )
+
@property
def froms(self):
"""Return the displayed list of FromClause elements."""
- return self._get_display_froms()
+ return self._compile_state_factory(self, None)._get_display_froms()
def with_statement_hint(self, text, dialect_name="*"):
"""add a statement hint to this :class:`.Select`.
@@ -3706,18 +3584,6 @@ class Select(
else:
self._hints = self._hints.union({(selectable, dialect_name): text})
- @_memoized_property.method
- def locate_all_froms(self):
- """return a Set of all FromClause elements referenced by this Select.
-
- This set is a superset of that returned by the ``froms`` property,
- which is specifically for those FromClause elements that would
- actually be rendered.
-
- """
- froms = self._froms
- return froms + list(_from_objects(*froms))
-
@property
def inner_columns(self):
"""an iterator of all ColumnElement expressions which would
@@ -3726,29 +3592,11 @@ class Select(
"""
return _select_iterables(self._raw_columns)
- @_memoized_property
- def _label_resolve_dict(self):
- with_cols = dict(
- (c._resolve_label or c._label or c.key, c)
- for c in _select_iterables(self._raw_columns)
- if c._allow_label_resolve
- )
- only_froms = dict(
- (c.key, c)
- for c in _select_iterables(self.froms)
- if c._allow_label_resolve
- )
- only_cols = with_cols.copy()
- for key, value in only_froms.items():
- with_cols.setdefault(key, value)
-
- return with_cols, only_froms, only_cols
-
def is_derived_from(self, fromclause):
if self in fromclause._cloned_set:
return True
- for f in self.locate_all_froms():
+ for f in self._iterate_from_elements():
if f.is_derived_from(fromclause):
return True
return False
@@ -3759,40 +3607,24 @@ class Select(
# objects
# 1. keep a dictionary of the froms we've cloned, and what
- # they've become. This is consulted later when we derive
- # additional froms from "whereclause" and the columns clause,
- # which may still reference the uncloned parent table.
- # as of 0.7.4 we also put the current version of _froms, which
- # gets cleared on each generation. previously we were "baking"
- # _froms into self._from_obj.
+ # they've become. This allows us to ensure the same cloned from
+ # is used when other items such as columns are "cloned"
all_the_froms = list(
itertools.chain(
_from_objects(*self._raw_columns),
- _from_objects(self._whereclause)
- if self._whereclause is not None
- else (),
+ _from_objects(*self._where_criteria),
)
)
new_froms = {f: clone(f, **kw) for f in all_the_froms}
- # copy FROM collections
- self._from_obj = util.OrderedSet(
- clone(f, **kw) for f in self._from_obj
- ).union(f for f in new_froms.values() if isinstance(f, Join))
-
- self._correlate = set(clone(f, **kw) for f in self._correlate)
- if self._correlate_except:
- self._correlate_except = set(
- clone(f, **kw) for f in self._correlate_except
- )
+ # 2. copy FROM collections.
+ self._from_obj = tuple(clone(f, **kw) for f in self._from_obj) + tuple(
+ f for f in new_froms.values() if isinstance(f, Join)
+ )
- # 4. clone other things. The difficulty here is that Column
- # objects are usually not altered by a straight clone because they
- # are dependent on the FROM cloning we just did above in order to
- # be targeted correctly, or a new FROM we have might be a JOIN
- # object which doesn't have its own columns. so give the cloner a
- # hint.
+ # 3. clone everything else, making sure we use columns
+ # corresponding to the froms we just made.
def replace(obj, **kw):
if isinstance(obj, ColumnClause) and obj.table in new_froms:
newelem = new_froms[obj.table].corresponding_column(obj)
@@ -3800,25 +3632,16 @@ class Select(
kw["replace"] = replace
- # TODO: I'd still like to try to leverage the traversal data
- self._raw_columns = [clone(c, **kw) for c in self._raw_columns]
- for attr in (
- "_limit_clause",
- "_offset_clause",
- "_whereclause",
- "_having",
- "_order_by_clause",
- "_group_by_clause",
- "_for_update_arg",
- ):
- if getattr(self, attr) is not None:
- setattr(self, attr, clone(getattr(self, attr), **kw))
+ super(Select, self)._copy_internals(
+ clone=clone, omit_attrs=("_from_obj",), **kw
+ )
self._reset_memoizations()
def get_children(self, **kwargs):
- # TODO: define "get_children" traversal items separately?
- return self._froms + super(Select, self).get_children(
+ return list(set(self._iterate_from_elements())) + super(
+ Select, self
+ ).get_children(
omit_attrs=["_from_obj", "_correlate", "_correlate_except"]
)
@@ -3839,7 +3662,7 @@ class Select(
self._reset_memoizations()
self._raw_columns = self._raw_columns + [
- coercions.expect(roles.ColumnsClauseRole, column)
+ coercions.expect(roles.ColumnsClauseRole, column,)
for column in columns
]
@@ -3890,7 +3713,7 @@ class Select(
sqlutil.reduce_columns(
self.inner_columns,
only_synonyms=only_synonyms,
- *(self._whereclause,) + tuple(self._from_obj)
+ *(self._where_criteria + self._from_obj)
)
)
@@ -3966,12 +3789,18 @@ class Select(
self._reset_memoizations()
rc = []
for c in columns:
- c = coercions.expect(roles.ColumnsClauseRole, c)
+ c = coercions.expect(roles.ColumnsClauseRole, c,)
if isinstance(c, ScalarSelect):
c = c.self_group(against=operators.comma_op)
rc.append(c)
self._raw_columns = rc
+ @property
+ def _whereclause(self):
+ """Legacy, return the WHERE clause as a :class:`.BooleanClauseList`"""
+
+ return and_(*self._where_criteria)
+
@_generative
def where(self, whereclause):
"""return a new select() construct with the given expression added to
@@ -3979,8 +3808,9 @@ class Select(
"""
- self._reset_memoizations()
- self._whereclause = and_(True_._ifnone(self._whereclause), whereclause)
+ self._where_criteria += (
+ coercions.expect(roles.WhereHavingRole, whereclause),
+ )
@_generative
def having(self, having):
@@ -3988,8 +3818,9 @@ class Select(
its HAVING clause, joined to the existing clause via AND, if any.
"""
- self._reset_memoizations()
- self._having = and_(True_._ifnone(self._having), having)
+ self._having_criteria += (
+ coercions.expect(roles.WhereHavingRole, having),
+ )
@_generative
def distinct(self, *expr):
@@ -4002,16 +3833,17 @@ class Select(
"""
if expr:
- expr = [coercions.expect(roles.ByOfRole, e) for e in expr]
self._distinct = True
- self._distinct_on = self._distinct_on + tuple(expr)
+ self._distinct_on = self._distinct_on + tuple(
+ coercions.expect(roles.ByOfRole, e) for e in expr
+ )
else:
self._distinct = True
@_generative
- def select_from(self, fromclause):
+ def select_from(self, *froms):
r"""return a new :func:`.select` construct with the
- given FROM expression
+ given FROM expression(s)
merged into its list of FROM objects.
E.g.::
@@ -4040,9 +3872,11 @@ class Select(
select([func.count('*')]).select_from(table1)
"""
- self._reset_memoizations()
- fromclause = coercions.expect(roles.FromClauseRole, fromclause)
- self._from_obj = self._from_obj.union([fromclause])
+
+ self._from_obj += tuple(
+ coercions.expect(roles.FromClauseRole, fromclause)
+ for fromclause in froms
+ )
@_generative
def correlate(self, *fromclauses):
@@ -4199,12 +4033,17 @@ class Select(
names = {}
def name_for_col(c):
- if c._label is None or not c._render_label_in_columns_clause:
+ if not c._render_label_in_columns_clause:
return (None, c, False)
+ elif c._label is None:
+ repeated = c.anon_label in names
+ names[c.anon_label] = c
+ return (None, c, repeated)
- repeated = False
name = c._label
+ repeated = False
+
if name in names:
# when looking to see if names[name] is the same column as
# c, use hash(), so that an annotated version of the column
@@ -4243,13 +4082,11 @@ class Select(
return [name_for_col(c) for c in cols]
else:
# repeated name logic only for use labels at the moment
- return [(None, c, False) for c in cols]
-
- @_memoized_property
- def _columns_plus_names(self):
- """generate label names plus columns to render in a SELECT."""
+ same_cols = set()
- return self._generate_columns_plus_names(True)
+ return [
+ (None, c, c in same_cols or same_cols.add(c)) for c in cols
+ ]
def _generate_fromclause_column_proxies(self, subquery):
"""generate column proxies to place in the exported .c collection
@@ -4341,29 +4178,34 @@ class Select(
"""
return CompoundSelect._create_intersect_all(self, other, **kwargs)
+ @property
def bind(self):
if self._bind:
return self._bind
- froms = self._froms
- if not froms:
- for c in self._raw_columns:
- e = c.bind
- if e:
- self._bind = e
- return e
- else:
- e = list(froms)[0].bind
+
+ for item in self._iterate_from_elements():
+ if item._is_subquery and item.element is self:
+ raise exc.InvalidRequestError(
+ "select() construct refers to itself as a FROM"
+ )
+
+ e = item.bind
if e:
self._bind = e
return e
+ else:
+ break
- return None
+ for c in self._raw_columns:
+ e = c.bind
+ if e:
+ self._bind = e
+ return e
- def _set_bind(self, bind):
+ @bind.setter
+ def bind(self, bind):
self._bind = bind
- bind = property(bind, _set_bind)
-
class ScalarSelect(roles.InElementRole, Generative, Grouping):
_from_objects = []
diff --git a/lib/sqlalchemy/sql/traversals.py b/lib/sqlalchemy/sql/traversals.py
index c29a04ee0..cd1583ef5 100644
--- a/lib/sqlalchemy/sql/traversals.py
+++ b/lib/sqlalchemy/sql/traversals.py
@@ -459,6 +459,11 @@ class _CopyInternals(InternalTraversal):
def visit_clauseelement_list(self, parent, element, clone=_clone, **kw):
return [clone(clause, **kw) for clause in element]
+ def visit_clauseelement_unordered_set(
+ self, parent, element, clone=_clone, **kw
+ ):
+ return {clone(clause, **kw) for clause in element}
+
def visit_clauseelement_tuples(self, parent, element, clone=_clone, **kw):
return [
tuple(clone(tup_elem, **kw) for tup_elem in elem)