From 693938dd6fb2f3ee3e031aed4c62355ac97f3ceb Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Fri, 6 Mar 2020 16:04:46 -0500 Subject: 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 --- lib/sqlalchemy/sql/selectable.py | 706 +++++++++++++++------------------------ 1 file changed, 274 insertions(+), 432 deletions(-) (limited to 'lib/sqlalchemy/sql/selectable.py') 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() " + "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() " - "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 = [] -- cgit v1.2.1