diff options
Diffstat (limited to 'lib/sqlalchemy/sql')
-rw-r--r-- | lib/sqlalchemy/sql/__init__.py | 1 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/base.py | 144 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/clause_compare.py | 2 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/coercions.py | 126 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/compiler.py | 100 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/dml.py | 4 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/elements.py | 109 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/expression.py | 5 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/roles.py | 10 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/selectable.py | 1012 | ||||
-rw-r--r-- | lib/sqlalchemy/sql/util.py | 2 |
11 files changed, 994 insertions, 521 deletions
diff --git a/lib/sqlalchemy/sql/__init__.py b/lib/sqlalchemy/sql/__init__.py index 00cafd8ff..bf5468aa5 100644 --- a/lib/sqlalchemy/sql/__init__.py +++ b/lib/sqlalchemy/sql/__init__.py @@ -58,6 +58,7 @@ from .expression import quoted_name # noqa from .expression import Select # noqa from .expression import select # noqa from .expression import Selectable # noqa +from .expression import Subquery # noqa from .expression import subquery # noqa from .expression import table # noqa from .expression import TableClause # noqa diff --git a/lib/sqlalchemy/sql/base.py b/lib/sqlalchemy/sql/base.py index 9df0c932f..a84843c4b 100644 --- a/lib/sqlalchemy/sql/base.py +++ b/lib/sqlalchemy/sql/base.py @@ -11,6 +11,7 @@ import itertools +import operator import re from .visitors import ClauseVisitor @@ -51,6 +52,38 @@ def _generative(fn, *args, **kw): return self +def _clone(element, **kw): + return element._clone() + + +def _expand_cloned(elements): + """expand the given set of ClauseElements to be the set of all 'cloned' + predecessors. + + """ + return itertools.chain(*[x._cloned_set for x in elements]) + + +def _cloned_intersection(a, b): + """return the intersection of sets a and b, counting + any overlap between 'cloned' predecessors. + + The returned set is in terms of the entities present within 'a'. + + """ + all_overlap = set(_expand_cloned(a)).intersection(_expand_cloned(b)) + return set( + elem for elem in a if all_overlap.intersection(elem._cloned_set) + ) + + +def _cloned_difference(a, b): + all_overlap = set(_expand_cloned(a)).intersection(_expand_cloned(b)) + return set( + elem for elem in a if not all_overlap.intersection(elem._cloned_set) + ) + + class _DialectArgView(util.collections_abc.MutableMapping): """A dictionary view of dialect-level arguments in the form <dialectname>_<argument_name>. @@ -486,6 +519,97 @@ class ColumnCollection(util.OrderedProperties): def __str__(self): return repr([str(c) for c in self]) + def corresponding_column(self, column, require_embedded=False): + """Given a :class:`.ColumnElement`, return the exported + :class:`.ColumnElement` object from this :class:`.ColumnCollection` + which corresponds to that original :class:`.ColumnElement` via a common + ancestor column. + + :param column: the target :class:`.ColumnElement` to be matched + + :param require_embedded: only return corresponding columns for + the given :class:`.ColumnElement`, if the given + :class:`.ColumnElement` is actually present within a sub-element + of this :class:`.Selectable`. Normally the column will match if + it merely shares a common ancestor with one of the exported + columns of this :class:`.Selectable`. + + .. seealso:: + + :meth:`.Selectable.corresponding_column` - invokes this method + against the collection returned by + :attr:`.Selectable.exported_columns`. + + .. versionchanged:: 1.4 the implementation for ``corresponding_column`` + was moved onto the :class:`.ColumnCollection` itself. + + """ + + def embedded(expanded_proxy_set, target_set): + for t in target_set.difference(expanded_proxy_set): + if not set(_expand_cloned([t])).intersection( + expanded_proxy_set + ): + return False + return True + + # don't dig around if the column is locally present + if self.contains_column(column): + return column + col, intersect = None, None + target_set = column.proxy_set + cols = self._all_columns + for c in cols: + expanded_proxy_set = set(_expand_cloned(c.proxy_set)) + i = target_set.intersection(expanded_proxy_set) + if i and ( + not require_embedded + or embedded(expanded_proxy_set, target_set) + ): + if col is None: + + # no corresponding column yet, pick this one. + + col, intersect = c, i + elif len(i) > len(intersect): + + # 'c' has a larger field of correspondence than + # 'col'. i.e. selectable.c.a1_x->a1.c.x->table.c.x + # matches a1.c.x->table.c.x better than + # selectable.c.x->table.c.x does. + + col, intersect = c, i + elif i == intersect: + # they have the same field of correspondence. see + # which proxy_set has fewer columns in it, which + # indicates a closer relationship with the root + # column. Also take into account the "weight" + # attribute which CompoundSelect() uses to give + # higher precedence to columns based on vertical + # position in the compound statement, and discard + # columns that have no reference to the target + # column (also occurs with CompoundSelect) + + col_distance = util.reduce( + operator.add, + [ + sc._annotations.get("weight", 1) + for sc in col._uncached_proxy_set() + if sc.shares_lineage(column) + ], + ) + c_distance = util.reduce( + operator.add, + [ + sc._annotations.get("weight", 1) + for sc in c._uncached_proxy_set() + if sc.shares_lineage(column) + ], + ) + if c_distance < col_distance: + col, intersect = c, i + return col + def replace(self, column): """add the given column to this collection, removing unaliased versions of this column as well as existing columns with the @@ -619,6 +743,26 @@ class ColumnCollection(util.OrderedProperties): return ImmutableColumnCollection(self._data, self._all_columns) +class SeparateKeyColumnCollection(ColumnCollection): + """Column collection that maintains a string name separate from the + column itself""" + + def __init__(self, cols_plus_names=None): + super(ColumnCollection, self).__init__() + object.__setattr__(self, "_all_columns", []) + if cols_plus_names: + self.update(cols_plus_names) + + def replace(self, column): + raise NotImplementedError() + + def add(self, column): + raise NotImplementedError() + + def remove(self, column): + raise NotImplementedError() + + class ImmutableColumnCollection(util.ImmutableProperties, ColumnCollection): def __init__(self, data, all_columns): util.ImmutableProperties.__init__(self, data) diff --git a/lib/sqlalchemy/sql/clause_compare.py b/lib/sqlalchemy/sql/clause_compare.py index 0ea981f1e..50b1df99e 100644 --- a/lib/sqlalchemy/sql/clause_compare.py +++ b/lib/sqlalchemy/sql/clause_compare.py @@ -291,7 +291,7 @@ class StructureComparatorStrategy(object): return True - def compare_text_as_from(self, left, right, **kw): + def compare_textual_select(self, left, right, **kw): self.compare_stack.extendleft( util.zip_longest(left.column_args, right.column_args) ) diff --git a/lib/sqlalchemy/sql/coercions.py b/lib/sqlalchemy/sql/coercions.py index 39e6628e4..64d9f0f96 100644 --- a/lib/sqlalchemy/sql/coercions.py +++ b/lib/sqlalchemy/sql/coercions.py @@ -124,18 +124,24 @@ class RoleImpl(object): return element def _implicit_coercions(self, element, resolved, argname=None, **kw): - self._raise_for_expected(element, argname) + self._raise_for_expected(element, argname, resolved) - def _raise_for_expected(self, element, argname=None): + def _raise_for_expected( + self, element, argname=None, resolved=None, advice=None, code=None + ): if argname: - raise exc.ArgumentError( - "%s expected for argument %r; got %r." - % (self.name, argname, element) + msg = "%s expected for argument %r; got %r." % ( + self.name, + argname, + element, ) else: - raise exc.ArgumentError( - "%s expected, got %r." % (self.name, element) - ) + msg = "%s expected, got %r." % (self.name, element) + + if advice: + msg += " " + advice + + raise exc.ArgumentError(msg, code=code) class _StringOnly(object): @@ -150,7 +156,7 @@ class _ReturnsStringKey(object): if isinstance(original_element, util.string_types): return original_element else: - self._raise_for_expected(original_element, argname) + self._raise_for_expected(original_element, argname, resolved) def _literal_coercion(self, element, **kw): return element @@ -172,15 +178,13 @@ class _ColumnCoercions(object): if resolved._is_select_statement: self._warn_for_scalar_subquery_coercion() return resolved.scalar_subquery() - elif ( - resolved._is_from_clause - and isinstance(resolved, selectable.Alias) - and resolved.element._is_select_statement + elif resolved._is_from_clause and isinstance( + resolved, selectable.Subquery ): self._warn_for_scalar_subquery_coercion() return resolved.element.scalar_subquery() else: - self._raise_for_expected(original_element, argname) + self._raise_for_expected(original_element, argname, resolved) def _no_text_coercion( @@ -236,6 +240,30 @@ class _CoerceLiterals(object): self._raise_for_expected(element, argname) +class _SelectIsNotFrom(object): + def _raise_for_expected(self, element, argname=None, resolved=None, **kw): + if isinstance(element, roles.SelectStatementRole) or isinstance( + resolved, roles.SelectStatementRole + ): + advice = ( + "To create a " + "FROM clause from a %s object, use the .subquery() method." + % (element.__class__) + ) + code = "89ve" + else: + advice = code = None + + return super(_SelectIsNotFrom, self)._raise_for_expected( + element, + argname=argname, + resolved=resolved, + advice=advice, + code=code, + **kw + ) + + class ExpressionElementImpl( _ColumnCoercions, RoleImpl, roles.ExpressionElementRole ): @@ -287,7 +315,7 @@ class InElementImpl(RoleImpl, roles.InElementRole): else: return resolved.select() else: - self._raise_for_expected(original_element, argname) + self._raise_for_expected(original_element, argname, resolved) def _literal_coercion(self, element, expr, operator, **kw): if isinstance(element, collections_abc.Iterable) and not isinstance( @@ -412,7 +440,7 @@ class TruncatedLabelImpl(_StringOnly, RoleImpl, roles.TruncatedLabelRole): if isinstance(original_element, util.string_types): return resolved else: - self._raise_for_expected(original_element, argname) + self._raise_for_expected(original_element, argname, resolved) def _literal_coercion(self, element, argname=None, **kw): """coerce the given value to :class:`._truncated_label`. @@ -447,7 +475,7 @@ class LimitOffsetImpl(RoleImpl, roles.LimitOffsetRole): if resolved is None: return None else: - self._raise_for_expected(element, argname) + self._raise_for_expected(element, argname, resolved) def _literal_coercion(self, element, name, type_, **kw): if element is None: @@ -474,10 +502,12 @@ class LabeledColumnExprImpl( if isinstance(new, roles.ExpressionElementRole): return new.label(None) else: - self._raise_for_expected(original_element, argname) + self._raise_for_expected(original_element, argname, resolved) -class ColumnsClauseImpl(_CoerceLiterals, RoleImpl, roles.ColumnsClauseRole): +class ColumnsClauseImpl( + _SelectIsNotFrom, _CoerceLiterals, RoleImpl, roles.ColumnsClauseRole +): _coerce_consts = True _coerce_numerics = True @@ -526,21 +556,40 @@ class SelectStatementImpl( if resolved._is_text_clause: return resolved.columns() else: - self._raise_for_expected(original_element, argname) + self._raise_for_expected(original_element, argname, resolved) class HasCTEImpl(ReturnsRowsImpl, roles.HasCTERole): pass -class FromClauseImpl(_NoTextCoercion, RoleImpl, roles.FromClauseRole): +class FromClauseImpl( + _SelectIsNotFrom, _NoTextCoercion, RoleImpl, roles.FromClauseRole +): def _implicit_coercions( - self, original_element, resolved, argname=None, **kw + self, + original_element, + resolved, + argname=None, + explicit_subquery=False, + allow_select=True, + **kw ): - if resolved._is_text_clause: + if resolved._is_select_statement: + if explicit_subquery: + return resolved.subquery() + elif allow_select: + util.warn_deprecated( + "Implicit coercion of SELECT and textual SELECT " + "constructs into FROM clauses is deprecated; please call " + ".subquery() on any Core select or ORM Query object in " + "order to produce a subquery object." + ) + return resolved._implicit_subquery + elif resolved._is_text_clause: return resolved else: - self._raise_for_expected(original_element, argname) + self._raise_for_expected(original_element, argname, resolved) class StrictFromClauseImpl(FromClauseImpl, roles.StrictFromClauseRole): @@ -559,16 +608,16 @@ class StrictFromClauseImpl(FromClauseImpl, roles.StrictFromClauseRole): "on any Core select or ORM Query object in order to produce a " "subquery object." ) - return resolved.subquery() + return resolved._implicit_subquery else: - self._raise_for_expected(original_element, argname) + self._raise_for_expected(original_element, argname, resolved) class AnonymizedFromClauseImpl( StrictFromClauseImpl, roles.AnonymizedFromClauseRole ): - def _post_coercion(self, element, flat=False, **kw): - return element.alias(flat=flat) + def _post_coercion(self, element, flat=False, name=None, **kw): + return element.alias(name=name, flat=flat) class DMLSelectImpl(_NoTextCoercion, RoleImpl, roles.DMLSelectRole): @@ -584,17 +633,28 @@ class DMLSelectImpl(_NoTextCoercion, RoleImpl, roles.DMLSelectRole): else: return resolved.select() else: - self._raise_for_expected(original_element, argname) + self._raise_for_expected(original_element, argname, resolved) class CompoundElementImpl( _NoTextCoercion, RoleImpl, roles.CompoundElementRole ): - def _implicit_coercions(self, original_element, resolved, argname=None): - if resolved._is_from_clause: - return resolved + def _raise_for_expected(self, element, argname=None, resolved=None, **kw): + if isinstance(element, roles.FromClauseRole): + if element._is_subquery: + advice = ( + "Use the plain select() object without " + "calling .subquery() or .alias()." + ) + else: + advice = ( + "To SELECT from any FROM clause, use the .select() method." + ) else: - self._raise_for_expected(original_element, argname) + advice = None + return super(CompoundElementImpl, self)._raise_for_expected( + element, argname=argname, resolved=resolved, advice=advice, **kw + ) _impl_lookup = {} diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 7922054f8..13219ee68 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -488,7 +488,7 @@ class SQLCompiler(Compiled): """ if False, means we can't be sure the list of entries in _result_columns is actually the rendered order. Usually - True unless using an unordered TextAsFrom. + True unless using an unordered TextualSelect. """ _numeric_binds = False @@ -916,8 +916,8 @@ class SQLCompiler(Compiled): ), ) - def visit_text_as_from( - self, taf, compound_index=None, asfrom=False, parens=True, **kw + def visit_textual_select( + self, taf, compound_index=None, asfrom=False, **kw ): toplevel = not self.stack @@ -943,10 +943,7 @@ class SQLCompiler(Compiled): add_to_result_map=self._add_to_result_map, ) - text = self.process(taf.element, **kw) - if asfrom and parens: - text = "(%s)" % text - return text + return self.process(taf.element, **kw) def visit_null(self, expr, **kw): return "NULL" @@ -1120,7 +1117,7 @@ class SQLCompiler(Compiled): return func.clause_expr._compiler_dispatch(self, **kwargs) def visit_compound_select( - self, cs, asfrom=False, parens=True, compound_index=0, **kwargs + self, cs, asfrom=False, compound_index=0, **kwargs ): toplevel = not self.stack entry = self._default_stack_entry if toplevel else self.stack[-1] @@ -1143,16 +1140,13 @@ class SQLCompiler(Compiled): text = (" " + keyword + " ").join( ( c._compiler_dispatch( - self, - asfrom=asfrom, - parens=False, - compound_index=i, - **kwargs + self, asfrom=asfrom, compound_index=i, **kwargs ) for i, c in enumerate(cs.selects) ) ) + kwargs["include_table"] = False text += self.group_by_clause(cs, **dict(asfrom=asfrom, **kwargs)) text += self.order_by_clause(cs, **kwargs) text += ( @@ -1165,10 +1159,7 @@ class SQLCompiler(Compiled): text = self._render_cte_clause() + text self.stack.pop(-1) - if asfrom and parens: - return "(" + text + ")" - else: - return text + return text def _get_operator_dispatch(self, operator_, qualifier1, qualifier2): attrname = "visit_%s_%s%s" % ( @@ -1682,8 +1673,11 @@ class SQLCompiler(Compiled): if self.positional: kwargs["positional_names"] = self.cte_positional[cte] = [] - text += " AS \n" + cte.element._compiler_dispatch( - self, asfrom=True, **kwargs + assert kwargs.get("subquery", False) is False + text += " AS \n(%s)" % ( + cte.element._compiler_dispatch( + self, asfrom=True, **kwargs + ), ) if cte._suffixes: @@ -1713,8 +1707,28 @@ class SQLCompiler(Compiled): ashint=False, iscrud=False, fromhints=None, + subquery=False, + lateral=False, + enclosing_alias=None, **kwargs ): + if enclosing_alias is not None and enclosing_alias.element is alias: + inner = alias.element._compiler_dispatch( + self, + asfrom=asfrom, + ashint=ashint, + iscrud=iscrud, + fromhints=fromhints, + lateral=lateral, + enclosing_alias=alias, + **kwargs + ) + if subquery and (asfrom or lateral): + inner = "(%s)" % (inner,) + return inner + else: + enclosing_alias = kwargs["enclosing_alias"] = alias + if asfrom or ashint: if isinstance(alias.name, elements._truncated_label): alias_name = self._truncated_identifier("alias", alias.name) @@ -1724,12 +1738,15 @@ class SQLCompiler(Compiled): if ashint: return self.preparer.format_alias(alias, alias_name) elif asfrom: - ret = alias.element._compiler_dispatch( - self, asfrom=True, **kwargs - ) + self.get_render_as_alias_suffix( - self.preparer.format_alias(alias, alias_name) + inner = alias.element._compiler_dispatch( + self, asfrom=True, lateral=lateral, **kwargs ) + if subquery: + inner = "(%s)" % (inner,) + ret = inner + self.get_render_as_alias_suffix( + self.preparer.format_alias(alias, alias_name) + ) if fromhints and alias in fromhints: ret = self.format_from_hint_text( ret, alias, fromhints[alias], iscrud @@ -1737,7 +1754,14 @@ class SQLCompiler(Compiled): return ret else: - return alias.element._compiler_dispatch(self, **kwargs) + # note we cancel the "subquery" flag here as well + return alias.element._compiler_dispatch( + self, lateral=lateral, **kwargs + ) + + def visit_subquery(self, subquery, **kw): + kw["subquery"] = True + return self.visit_alias(subquery, **kw) def visit_lateral(self, lateral, **kw): kw["lateral"] = True @@ -2004,7 +2028,6 @@ class SQLCompiler(Compiled): self, select, asfrom=False, - parens=True, fromhints=None, compound_index=0, nested_join_translation=False, @@ -2027,7 +2050,6 @@ class SQLCompiler(Compiled): text = self.visit_select( transformed_select, asfrom=asfrom, - parens=parens, fromhints=fromhints, compound_index=compound_index, nested_join_translation=True, @@ -2138,10 +2160,7 @@ class SQLCompiler(Compiled): self.stack.pop(-1) - if (asfrom or lateral) and parens: - return "(" + text + ")" - else: - return text + return text def _setup_select_hints(self, select): byfrom = dict( @@ -2371,7 +2390,7 @@ class SQLCompiler(Compiled): ) return dialect_hints, table_text - def visit_insert(self, insert_stmt, asfrom=False, **kw): + def visit_insert(self, insert_stmt, **kw): toplevel = not self.stack self.stack.append( @@ -2475,10 +2494,7 @@ class SQLCompiler(Compiled): self.stack.pop(-1) - if asfrom: - return "(" + text + ")" - else: - return text + return text def update_limit_clause(self, update_stmt): """Provide a hook for MySQL to add LIMIT to the UPDATE""" @@ -2508,7 +2524,7 @@ class SQLCompiler(Compiled): "criteria within UPDATE" ) - def visit_update(self, update_stmt, asfrom=False, **kw): + def visit_update(self, update_stmt, **kw): toplevel = not self.stack extra_froms = update_stmt._extra_froms @@ -2605,10 +2621,7 @@ class SQLCompiler(Compiled): self.stack.pop(-1) - if asfrom: - return "(" + text + ")" - else: - return text + return text @util.memoized_property def _key_getters_for_crud_column(self): @@ -2633,7 +2646,7 @@ class SQLCompiler(Compiled): def delete_table_clause(self, delete_stmt, from_table, extra_froms): return from_table._compiler_dispatch(self, asfrom=True, iscrud=True) - def visit_delete(self, delete_stmt, asfrom=False, **kw): + def visit_delete(self, delete_stmt, **kw): toplevel = not self.stack crud._setup_crud_params(self, delete_stmt, crud.ISDELETE, **kw) @@ -2702,10 +2715,7 @@ class SQLCompiler(Compiled): self.stack.pop(-1) - if asfrom: - return "(" + text + ")" - else: - return text + return text def visit_savepoint(self, savepoint_stmt): return "SAVEPOINT %s" % self.preparer.format_savepoint(savepoint_stmt) diff --git a/lib/sqlalchemy/sql/dml.py b/lib/sqlalchemy/sql/dml.py index c7d83fc12..5a9be7c62 100644 --- a/lib/sqlalchemy/sql/dml.py +++ b/lib/sqlalchemy/sql/dml.py @@ -47,6 +47,10 @@ class UpdateBase( _prefixes = () named_with_column = False + def _generate_fromclause_column_proxies(self, fromclause): + for col in self._returning: + col._make_proxy(fromclause) + def _process_colparams(self, parameters): def process_single(p): if isinstance(p, (list, tuple)): diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index b902ef4b4..5b4442222 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -22,6 +22,7 @@ from . import operators from . import roles from . import type_api from .annotation import Annotated +from .base import _clone from .base import _generative from .base import Executable from .base import Immutable @@ -36,10 +37,6 @@ from .. import inspection from .. import util -def _clone(element, **kw): - return element._clone() - - def collate(expression, collation): """Return the clause ``expression COLLATE collation``. @@ -415,6 +412,11 @@ class ClauseElement(roles.SQLRole, Visitable): """ return self + def _ungroup(self): + """Return this :class:`.ClauseElement` without any groupings.""" + + return self + @util.dependencies("sqlalchemy.engine.default") def compile(self, default, bind=None, dialect=None, **kw): """Compile this SQL expression. @@ -821,6 +823,16 @@ class ColumnElement( and other.name == self.name ) + @util.memoized_property + def _proxy_key(self): + if self.key: + return self.key + else: + try: + return str(self) + except exc.UnsupportedCompilationError: + return self.anon_label + def _make_proxy( self, selectable, name=None, name_is_truncatable=False, **kw ): @@ -831,14 +843,7 @@ class ColumnElement( """ if name is None: name = self.anon_label - if self.key: - key = self.key - else: - try: - key = str(self) - except exc.UnsupportedCompilationError: - key = self.anon_label - + key = self._proxy_key else: key = name co = ColumnClause( @@ -1619,8 +1624,14 @@ class TextClause( @util.dependencies("sqlalchemy.sql.selectable") def columns(self, selectable, *cols, **types): - """Turn this :class:`.TextClause` object into a :class:`.TextAsFrom` - object that can be embedded into another statement. + r"""Turn this :class:`.TextClause` object into a + :class:`.TextualSelect` object that serves the same role as a SELECT + statement. + + The :class:`.TextualSelect` is part of the :class:`.SelectBase` + hierarchy and can be embedded into another statement by using the + :meth:`.TextualSelect.subquery` method to produce a :class:`.Subquery` + object, which can then be SELECTed from. This function essentially bridges the gap between an entirely textual SELECT statement and the SQL expression language concept @@ -1629,7 +1640,7 @@ class TextClause( from sqlalchemy.sql import column, text stmt = text("SELECT id, name FROM some_table") - stmt = stmt.columns(column('id'), column('name')).alias('st') + stmt = stmt.columns(column('id'), column('name')).subquery('st') stmt = select([mytable]).\ select_from( @@ -1638,8 +1649,10 @@ class TextClause( Above, we pass a series of :func:`.column` elements to the :meth:`.TextClause.columns` method positionally. These :func:`.column` - elements now become first class elements upon the :attr:`.TextAsFrom.c` - column collection, just like any other selectable. + elements now become first class elements upon the + :attr:`.TextualSelect.selected_columns` column collection, which then + become part of the :attr:`.Subquery.c` collection after + :meth:`.TextualSelect.subquery` is invoked. The column expressions we pass to :meth:`.TextClause.columns` may also be typed; when we do so, these :class:`.TypeEngine` objects become @@ -1697,17 +1710,22 @@ class TextClause( the column expressions are passed purely positionally. The :meth:`.TextClause.columns` method provides a direct - route to calling :meth:`.FromClause.alias` as well as + route to calling :meth:`.FromClause.subquery` as well as :meth:`.SelectBase.cte` against a textual SELECT statement:: stmt = stmt.columns(id=Integer, name=String).cte('st') stmt = select([sometable]).where(sometable.c.id == stmt.c.id) - .. versionadded:: 0.9.0 :func:`.text` can now be converted into a - fully featured "selectable" construct using the - :meth:`.TextClause.columns` method. + :param \*cols: A series of :class:`.ColumnElement` objects, typically + :class:`.Column` objects from a :class:`.Table` or ORM level + column-mapped attributes, representing a set of columns that this + textual string will SELECT from. + :param \**types: A mapping of string names to :class:`.TypeEngine` + type objects indicating the datatypes to use for names that are + SELECTed from the textual string. Prefer to use the ``\*cols`` + argument as it also indicates positional ordering. """ positional_input_cols = [ @@ -1720,7 +1738,7 @@ class TextClause( ColumnClause(key, type_) for key, type_ in types.items() ] - return selectable.TextAsFrom( + return selectable.TextualSelect( self, positional_input_cols + keyed_input_cols, positional=bool(positional_input_cols) and not keyed_input_cols, @@ -3291,19 +3309,26 @@ class IndexExpression(BinaryExpression): pass -class Grouping(ColumnElement): - """Represent a grouping within a column expression""" +class GroupedElement(ClauseElement): + """Represent any parenthesized expression""" __visit_name__ = "grouping" - def __init__(self, element): - self.element = element - self.type = getattr(element, "type", type_api.NULLTYPE) - def self_group(self, against=None): # type: (Optional[Any]) -> ClauseElement return self + def _ungroup(self): + return self.element._ungroup() + + +class Grouping(GroupedElement, ColumnElement): + """Represent a grouping within a column expression""" + + def __init__(self, element): + self.element = element + self.type = getattr(element, "type", type_api.NULLTYPE) + @util.memoized_property def _is_implicitly_boolean(self): return self.element._is_implicitly_boolean @@ -4351,14 +4376,6 @@ class quoted_name(util.MemoizedSlots, util.text_type): return "'%s'" % backslashed -def _expand_cloned(elements): - """expand the given set of ClauseElements to be the set of all 'cloned' - predecessors. - - """ - return itertools.chain(*[x._cloned_set for x in elements]) - - def _select_iterables(elements): """expand tables into individual columns in the given list of column expressions. @@ -4367,26 +4384,6 @@ def _select_iterables(elements): return itertools.chain(*[c._select_iterable for c in elements]) -def _cloned_intersection(a, b): - """return the intersection of sets a and b, counting - any overlap between 'cloned' predecessors. - - The returned set is in terms of the entities present within 'a'. - - """ - all_overlap = set(_expand_cloned(a)).intersection(_expand_cloned(b)) - return set( - elem for elem in a if all_overlap.intersection(elem._cloned_set) - ) - - -def _cloned_difference(a, b): - all_overlap = set(_expand_cloned(a)).intersection(_expand_cloned(b)) - return set( - elem for elem in a if not all_overlap.intersection(elem._cloned_set) - ) - - 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 b04355cf5..7ce822669 100644 --- a/lib/sqlalchemy/sql/expression.py +++ b/lib/sqlalchemy/sql/expression.py @@ -16,6 +16,7 @@ class. __all__ = [ "Alias", + "AliasedReturnsRows", "any_", "all_", "ClauseElement", @@ -76,6 +77,7 @@ __all__ = [ "union_all", "update", "within_group", + "Subquery", "TableSample", "tablesample", ] @@ -132,6 +134,7 @@ from .functions import Function # noqa from .functions import FunctionElement # noqa from .functions import modifier # noqa from .selectable import Alias # noqa +from .selectable import AliasedReturnsRows # noqa from .selectable import CompoundSelect # noqa from .selectable import CTE # noqa from .selectable import Exists # noqa @@ -148,10 +151,12 @@ from .selectable import ScalarSelect # noqa from .selectable import Select # noqa from .selectable import Selectable # noqa from .selectable import SelectBase # noqa +from .selectable import Subquery # noqa from .selectable import subquery # noqa from .selectable import TableClause # noqa from .selectable import TableSample # noqa from .selectable import TextAsFrom # noqa +from .selectable import TextualSelect # noqa from .visitors import Visitable # noqa from ..util.langhelpers import public_factory # noqa diff --git a/lib/sqlalchemy/sql/roles.py b/lib/sqlalchemy/sql/roles.py index 053bd7146..55c52d401 100644 --- a/lib/sqlalchemy/sql/roles.py +++ b/lib/sqlalchemy/sql/roles.py @@ -95,6 +95,8 @@ class InElementRole(SQLRole): class FromClauseRole(ColumnsClauseRole): _role_name = "FROM expression, such as a Table or alias() object" + _is_subquery = False + @property def _hide_froms(self): raise NotImplementedError() @@ -134,7 +136,7 @@ class StatementRole(CoerceTextStatementRole): class ReturnsRowsRole(StatementRole): _role_name = ( - "Row returning expression such as a SELECT, or an " + "Row returning expression such as a SELECT, a FROM clause, or an " "INSERT/UPDATE/DELETE with RETURNING" ) @@ -142,6 +144,12 @@ class ReturnsRowsRole(StatementRole): class SelectStatementRole(ReturnsRowsRole): _role_name = "SELECT construct or equivalent text() construct" + def subquery(self): + raise NotImplementedError( + "All SelectStatementRole objects should implement a " + ".subquery() method." + ) + class HasCTERole(ReturnsRowsRole): pass diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py index 591086a46..2263073c4 100644 --- a/lib/sqlalchemy/sql/selectable.py +++ b/lib/sqlalchemy/sql/selectable.py @@ -12,7 +12,6 @@ SQL tables and derived rowsets. import collections import itertools -import operator from operator import attrgetter from . import coercions @@ -20,6 +19,10 @@ from . import operators from . import roles from . import type_api from .annotation import Annotated +from .base import _clone +from .base import _cloned_difference +from .base import _cloned_intersection +from .base import _expand_cloned from .base import _from_objects from .base import _generative from .base import ColumnCollection @@ -27,17 +30,15 @@ from .base import ColumnSet from .base import Executable from .base import Generative from .base import Immutable +from .base import SeparateKeyColumnCollection from .coercions import _document_text_coercion from .elements import _anonymous_label -from .elements import _clone -from .elements import _cloned_difference -from .elements import _cloned_intersection -from .elements import _expand_cloned from .elements import _select_iterables from .elements import and_ from .elements import BindParameter from .elements import ClauseElement from .elements import ClauseList +from .elements import GroupedElement from .elements import Grouping from .elements import literal_column from .elements import True_ @@ -52,20 +53,22 @@ class _OffsetLimitParam(BindParameter): return self.effective_value +@util.deprecated( + "1.4", + "The standalone :func:`.subquery` function is deprecated " + "and will be removed in a future release. Use select().subquery().", +) def subquery(alias, *args, **kwargs): - r"""Return an :class:`.Alias` object derived + r"""Return an :class:`.Subquery` object derived from a :class:`.Select`. - name - alias name + :param name: the alias name for the subquery - \*args, \**kwargs - - all other arguments are delivered to the - :func:`select` function. + :param \*args, \**kwargs: all other arguments are passed through to the + :func:`.select` function. """ - return Select(*args, **kwargs).alias(alias) + return Select(*args, **kwargs).subquery(alias) class ReturnsRows(roles.ReturnsRowsRole, ClauseElement): @@ -90,19 +93,12 @@ class ReturnsRows(roles.ReturnsRowsRole, ClauseElement): @property def selectable(self): - raise NotImplementedError( - "This object is a base ReturnsRows object, but is not a " - "FromClause so has no .c. collection." - ) + raise NotImplementedError() class Selectable(ReturnsRows): """mark a class as being selectable. - This class is legacy as of 1.4 as the concept of a SQL construct which - "returns rows" is more generalized than one which can be the subject - of a SELECT. - """ __visit_name__ = "selectable" @@ -113,6 +109,93 @@ class Selectable(ReturnsRows): def selectable(self): return self + @property + def exported_columns(self): + """A :class:`.ColumnCollection` that represents the "exported" + columns of this :class:`.Selectable`. + + The "exported" columns represent the collection of + :class:`.ColumnElement` expressions that are rendered by this SQL + construct. There are two primary varieties which are the + "FROM clause columns" of a FROM clause, such as a table, join, + or subquery, and the "SELECTed columns", which are the columns in + the "columns clause" of a SELECT statement. + + .. versionadded:: 1.4 + + .. seealso: + + :attr:`.FromClause.exported_columns` + + :attr:`.SelectBase.exported_columns` + """ + + raise NotImplementedError() + + def _refresh_for_new_column(self, column): + raise NotImplementedError() + + def lateral(self, name=None): + """Return a LATERAL alias of this :class:`.Selectable`. + + The return value is the :class:`.Lateral` construct also + provided by the top-level :func:`~.expression.lateral` function. + + .. versionadded:: 1.1 + + .. seealso:: + + :ref:`lateral_selects` - overview of usage. + + """ + return Lateral._construct(self, name) + + @util.deprecated( + "1.4", + message="The :meth:`.Selectable.replace_selectable` method is " + "deprecated, and will be removed in a future release. Similar " + "functionality is available via the sqlalchemy.sql.visitors module.", + ) + @util.dependencies("sqlalchemy.sql.util") + def replace_selectable(self, sqlutil, old, alias): + """replace all occurrences of FromClause 'old' with the given Alias + object, returning a copy of this :class:`.FromClause`. + + """ + + return sqlutil.ClauseAdapter(alias).traverse(self) + + def corresponding_column(self, column, require_embedded=False): + """Given a :class:`.ColumnElement`, return the exported + :class:`.ColumnElement` object from the + :attr:`.Selectable.exported_columns` + collection of this :class:`.Selectable` which corresponds to that + original :class:`.ColumnElement` via a common ancestor + column. + + :param column: the target :class:`.ColumnElement` to be matched + + :param require_embedded: only return corresponding columns for + the given :class:`.ColumnElement`, if the given + :class:`.ColumnElement` is actually present within a sub-element + of this :class:`.Selectable`. Normally the column will match if + it merely shares a common ancestor with one of the exported + columns of this :class:`.Selectable`. + + .. seealso:: + + :attr:`.Selectable.exported_columns` - the + :class:`.ColumnCollection` that is used for the operation + + :meth:`.ColumnCollection.corresponding_column` - implementation + method. + + """ + + return self.exported_columns.corresponding_column( + column, require_embedded + ) + class HasPrefixes(object): _prefixes = () @@ -211,7 +294,7 @@ class HasSuffixes(object): ) -class FromClause(roles.FromClauseRole, Selectable): +class FromClause(roles.AnonymizedFromClauseRole, Selectable): """Represent an element that can be used within the ``FROM`` clause of a ``SELECT`` statement. @@ -243,9 +326,8 @@ class FromClause(roles.FromClauseRole, Selectable): """ - is_selectable = has_selectable = True + is_selectable = True _is_from_clause = True - _is_text_as_from = False _is_join = False def _translate_schema(self, effective_schema, map_): @@ -400,21 +482,6 @@ class FromClause(roles.FromClauseRole, Selectable): return Alias._construct(self, name) - def lateral(self, name=None): - """Return a LATERAL alias of this :class:`.FromClause`. - - The return value is the :class:`.Lateral` construct also - provided by the top-level :func:`~.expression.lateral` function. - - .. versionadded:: 1.1 - - .. seealso:: - - :ref:`lateral_selects` - overview of usage. - - """ - return Lateral._construct(self, name) - def tablesample(self, sampling, name=None, seed=None): """Return a TABLESAMPLE alias of this :class:`.FromClause`. @@ -452,125 +519,43 @@ class FromClause(roles.FromClauseRole, Selectable): """ return self._cloned_set.intersection(other._cloned_set) - @util.dependencies("sqlalchemy.sql.util") - def replace_selectable(self, sqlutil, old, alias): - """replace all occurrences of FromClause 'old' with the given Alias - object, returning a copy of this :class:`.FromClause`. - - """ - - return sqlutil.ClauseAdapter(alias).traverse(self) + @property + def description(self): + """a brief description of this FromClause. - def correspond_on_equivalents(self, column, equivalents): - """Return corresponding_column for the given column, or if None - search for a match in the given dictionary. + Used primarily for error message formatting. """ - col = self.corresponding_column(column, require_embedded=True) - if col is None and col in equivalents: - for equiv in equivalents[col]: - nc = self.corresponding_column(equiv, require_embedded=True) - if nc: - return nc - return col + return getattr(self, "name", self.__class__.__name__ + " object") - def corresponding_column(self, column, require_embedded=False): - """Given a :class:`.ColumnElement`, return the exported - :class:`.ColumnElement` object from this :class:`.Selectable` - which corresponds to that original - :class:`~sqlalchemy.schema.Column` via a common ancestor - column. + def _reset_exported(self): + """delete memoized collections when a FromClause is cloned.""" - :param column: the target :class:`.ColumnElement` to be matched + self._memoized_property.expire_instance(self) - :param require_embedded: only return corresponding columns for - the given :class:`.ColumnElement`, if the given - :class:`.ColumnElement` is actually present within a sub-element - of this :class:`.FromClause`. Normally the column will match if - it merely shares a common ancestor with one of the exported - columns of this :class:`.FromClause`. + def _generate_fromclause_column_proxies(self, fromclause): + for col in self.c: + col._make_proxy(fromclause) - """ + @property + def exported_columns(self): + """A :class:`.ColumnCollection` that represents the "exported" + columns of this :class:`.Selectable`. - def embedded(expanded_proxy_set, target_set): - for t in target_set.difference(expanded_proxy_set): - if not set(_expand_cloned([t])).intersection( - expanded_proxy_set - ): - return False - return True + The "exported" columns for a :class:`.FromClause` object are synonymous + with the :attr:`.FromClause.columns` collection. - # don't dig around if the column is locally present - if self.c.contains_column(column): - return column - col, intersect = None, None - target_set = column.proxy_set - cols = self.c._all_columns - for c in cols: - expanded_proxy_set = set(_expand_cloned(c.proxy_set)) - i = target_set.intersection(expanded_proxy_set) - if i and ( - not require_embedded - or embedded(expanded_proxy_set, target_set) - ): - if col is None: - - # no corresponding column yet, pick this one. - - col, intersect = c, i - elif len(i) > len(intersect): - - # 'c' has a larger field of correspondence than - # 'col'. i.e. selectable.c.a1_x->a1.c.x->table.c.x - # matches a1.c.x->table.c.x better than - # selectable.c.x->table.c.x does. - - col, intersect = c, i - elif i == intersect: - - # they have the same field of correspondence. see - # which proxy_set has fewer columns in it, which - # indicates a closer relationship with the root - # column. Also take into account the "weight" - # attribute which CompoundSelect() uses to give - # higher precedence to columns based on vertical - # position in the compound statement, and discard - # columns that have no reference to the target - # column (also occurs with CompoundSelect) - - col_distance = util.reduce( - operator.add, - [ - sc._annotations.get("weight", 1) - for sc in col._uncached_proxy_set() - if sc.shares_lineage(column) - ], - ) - c_distance = util.reduce( - operator.add, - [ - sc._annotations.get("weight", 1) - for sc in c._uncached_proxy_set() - if sc.shares_lineage(column) - ], - ) - if c_distance < col_distance: - col, intersect = c, i - return col + .. versionadded:: 1.4 - @property - def description(self): - """a brief description of this FromClause. + .. seealso: - Used primarily for error message formatting. + :attr:`.Selectable.exported_columns` - """ - return getattr(self, "name", self.__class__.__name__ + " object") + :attr:`.SelectBase.exported_columns` - def _reset_exported(self): - """delete memoized collections when a FromClause is cloned.""" - self._memoized_property.expire_instance(self) + """ + return self.columns @_memoized_property def columns(self): @@ -660,15 +645,10 @@ class FromClause(roles.FromClauseRole, Selectable): derivations. """ - if not self._cols_populated: - return None - elif column.key in self.columns and self.columns[column.key] is column: - return column - else: - return None + self._reset_exported() -class Join(roles.AnonymizedFromClauseRole, FromClause): +class Join(FromClause): """represent a ``JOIN`` construct between two :class:`.FromClause` elements. @@ -811,23 +791,15 @@ class Join(roles.AnonymizedFromClauseRole, FromClause): (c for c in columns if c.primary_key), self.onclause ) ) - self._columns.update((col._label, col) for col in columns) + self._columns.update((col._key_label, col) for col in columns) self.foreign_keys.update( itertools.chain(*[col.foreign_keys for col in columns]) ) def _refresh_for_new_column(self, column): - col = self.left._refresh_for_new_column(column) - if col is None: - col = self.right._refresh_for_new_column(column) - if col is not None: - if self._cols_populated: - self._columns[col._label] = col - self.foreign_keys.update(col.foreign_keys) - if col.primary_key: - self.primary_key.add(col) - return col - return None + super(Join, self)._refresh_for_new_column(column) + self.left._refresh_for_new_column(column) + self.right._refresh_for_new_column(column) def _copy_internals(self, clone=_clone, **kw): self._reset_exported() @@ -947,6 +919,9 @@ class Join(roles.AnonymizedFromClauseRole, FromClause): def _joincond_scan_left_right( cls, a, a_subset, b, consider_as_foreign_keys ): + a = coercions.expect(roles.FromClauseRole, a) + b = coercions.expect(roles.FromClauseRole, b) + constraints = collections.defaultdict(list) for left in (a_subset, a): @@ -1186,23 +1161,20 @@ class Join(roles.AnonymizedFromClauseRole, FromClause): ) -class Alias(roles.AnonymizedFromClauseRole, FromClause): - """Represents an table or selectable alias (AS). - - Represents an alias, as typically applied to any table or - sub-select within a SQL statement using the ``AS`` keyword (or - without the keyword on certain databases such as Oracle). - - This object is constructed from the :func:`~.expression.alias` module - level function as well as the :meth:`.FromClause.alias` method available - on all :class:`.FromClause` subclasses. - - """ - - __visit_name__ = "alias" - named_with_column = True +# FromClause -> +# AliasedReturnsRows +# -> Alias only for FromClause +# -> Subquery only for SelectBase +# -> CTE only for HasCTE -> SelectBase, DML +# -> Lateral -> FromClause, but we accept SelectBase +# w/ non-deprecated coercion +# -> TableSample -> only for FromClause +class AliasedReturnsRows(FromClause): + """Base class of aliases against tables, subqueries, and other + selectables.""" _is_from_container = True + named_with_column = True def __init__(self, *arg, **kw): raise NotImplementedError( @@ -1224,59 +1196,12 @@ class Alias(roles.AnonymizedFromClauseRole, FromClause): return obj @classmethod - def _factory(cls, selectable, name=None, flat=False): - """Return an :class:`.Alias` object. - - An :class:`.Alias` represents any :class:`.FromClause` - with an alternate name assigned within SQL, typically using the ``AS`` - clause when generated, e.g. ``SELECT * FROM table AS aliasname``. - - Similar functionality is available via the - :meth:`~.FromClause.alias` method - available on all :class:`.FromClause` subclasses. In terms of a - SELECT object as generated from the :func:`.select` function, the - :meth:`.SelectBase.alias` method returns an :class:`.Alias` or - similar object which represents a named, parenthesized subquery. - - When an :class:`.Alias` is created from a :class:`.Table` object, - this has the effect of the table being rendered - as ``tablename AS aliasname`` in a SELECT statement. - - For :func:`.select` objects, the effect is that of creating a named - subquery, i.e. ``(select ...) AS aliasname``. - - The ``name`` parameter is optional, and provides the name - to use in the rendered SQL. If blank, an "anonymous" name - will be deterministically generated at compile time. - Deterministic means the name is guaranteed to be unique against - other constructs used in the same statement, and will also be the - same name for each successive compilation of the same statement - object. - - :param selectable: any :class:`.FromClause` subclass, - such as a table, select statement, etc. - - :param name: string name to be assigned as the alias. - If ``None``, a name will be deterministically generated - at compile time. - - :param flat: Will be passed through to if the given selectable - is an instance of :class:`.Join` - see :meth:`.Join.alias` - for details. - - .. versionadded:: 0.9.0 - - """ - return coercions.expect(roles.FromClauseRole, selectable).alias( - name=name, flat=flat - ) + def _factory(cls, returnsrows, name=None): + """Base factory method. Subclasses need to provide this.""" + raise NotImplementedError() def _init(self, selectable, name=None): - self.wrapped = selectable - if isinstance(selectable, Alias): - selectable = selectable.element - assert not isinstance(selectable, Alias) - + self.element = selectable self.supports_execution = selectable.supports_execution if self.supports_execution: self._execution_options = selectable._execution_options @@ -1288,18 +1213,14 @@ class Alias(roles.AnonymizedFromClauseRole, FromClause): and selectable.named_with_column ): name = getattr(selectable, "name", None) + if isinstance(name, _anonymous_label): + name = None name = _anonymous_label("%%(%d %s)s" % (id(self), name or "anon")) self.name = name - def self_group(self, against=None): - if ( - isinstance(against, CompoundSelect) - and isinstance(self.element, Select) - and self.element._needs_parens_for_grouping() - ): - return FromGrouping(self) - - return super(Alias, self).self_group(against=against) + def _refresh_for_new_column(self, column): + super(AliasedReturnsRows, self)._refresh_for_new_column(column) + self.element._refresh_for_new_column(column) @property def description(self): @@ -1319,18 +1240,7 @@ class Alias(roles.AnonymizedFromClauseRole, FromClause): return self.element.is_derived_from(fromclause) def _populate_column_collection(self): - for col in self.wrapped.columns._all_columns: - col._make_proxy(self) - - def _refresh_for_new_column(self, column): - col = self.wrapped._refresh_for_new_column(column) - if col is not None: - if not self._cols_populated: - return None - else: - return col._make_proxy(self) - else: - return None + self.element._generate_fromclause_column_proxies(self) def _copy_internals(self, clone=_clone, **kw): # don't apply anything to an aliased Table @@ -1339,17 +1249,13 @@ class Alias(roles.AnonymizedFromClauseRole, FromClause): if isinstance(self.element, TableClause): return self._reset_exported() - self.wrapped = clone(self.wrapped, **kw) - if isinstance(self.wrapped, Alias): - self.element = self.wrapped.element - else: - self.element = self.wrapped + self.element = clone(self.element, **kw) def get_children(self, column_collections=True, **kw): if column_collections: for c in self.c: yield c - yield self.wrapped + yield self.element def _cache_key(self, **kw): return (self.__class__, self.element._cache_key(**kw), self._orig_name) @@ -1363,7 +1269,71 @@ class Alias(roles.AnonymizedFromClauseRole, FromClause): return self.element.bind -class Lateral(Alias): +class Alias(AliasedReturnsRows): + """Represents an table or selectable alias (AS). + + Represents an alias, as typically applied to any table or + sub-select within a SQL statement using the ``AS`` keyword (or + without the keyword on certain databases such as Oracle). + + This object is constructed from the :func:`~.expression.alias` module + level function as well as the :meth:`.FromClause.alias` method available + on all :class:`.FromClause` subclasses. + + """ + + __visit_name__ = "alias" + + @classmethod + def _factory(cls, selectable, name=None, flat=False): + """Return an :class:`.Alias` object. + + An :class:`.Alias` represents any :class:`.FromClause` + with an alternate name assigned within SQL, typically using the ``AS`` + clause when generated, e.g. ``SELECT * FROM table AS aliasname``. + + Similar functionality is available via the + :meth:`~.FromClause.alias` method + available on all :class:`.FromClause` subclasses. In terms of a + SELECT object as generated from the :func:`.select` function, the + :meth:`.SelectBase.alias` method returns an :class:`.Alias` or + similar object which represents a named, parenthesized subquery. + + When an :class:`.Alias` is created from a :class:`.Table` object, + this has the effect of the table being rendered + as ``tablename AS aliasname`` in a SELECT statement. + + For :func:`.select` objects, the effect is that of creating a named + subquery, i.e. ``(select ...) AS aliasname``. + + The ``name`` parameter is optional, and provides the name + to use in the rendered SQL. If blank, an "anonymous" name + will be deterministically generated at compile time. + Deterministic means the name is guaranteed to be unique against + other constructs used in the same statement, and will also be the + same name for each successive compilation of the same statement + object. + + :param selectable: any :class:`.FromClause` subclass, + such as a table, select statement, etc. + + :param name: string name to be assigned as the alias. + If ``None``, a name will be deterministically generated + at compile time. + + :param flat: Will be passed through to if the given selectable + is an instance of :class:`.Join` - see :meth:`.Join.alias` + for details. + + .. versionadded:: 0.9.0 + + """ + return coercions.expect( + roles.FromClauseRole, selectable, allow_select=True + ).alias(name=name, flat=flat) + + +class Lateral(AliasedReturnsRows): """Represent a LATERAL subquery. This object is constructed from the :func:`~.expression.lateral` module @@ -1404,12 +1374,12 @@ class Lateral(Alias): :ref:`lateral_selects` - overview of usage. """ - return coercions.expect(roles.FromClauseRole, selectable).lateral( - name=name - ) + return coercions.expect( + roles.FromClauseRole, selectable, explicit_subquery=True + ).lateral(name=name) -class TableSample(Alias): +class TableSample(AliasedReturnsRows): """Represent a TABLESAMPLE clause. This object is constructed from the :func:`~.expression.tablesample` module @@ -1485,7 +1455,7 @@ class TableSample(Alias): return functions.func.system(self.sampling) -class CTE(Generative, HasSuffixes, Alias): +class CTE(Generative, HasSuffixes, AliasedReturnsRows): """Represent a Common Table Expression. The :class:`.CTE` object is obtained using the @@ -1531,15 +1501,6 @@ class CTE(Generative, HasSuffixes, Alias): [clone(elem, **kw) for elem in self._restates] ) - @util.dependencies("sqlalchemy.sql.dml") - def _populate_column_collection(self, dml): - if isinstance(self.element, dml.UpdateBase): - for col in self.element._returning: - col._make_proxy(self) - else: - for col in self.element.columns._all_columns: - col._make_proxy(self) - def alias(self, name=None, flat=False): """Return an :class:`.Alias` of this :class:`.CTE`. @@ -1748,13 +1709,26 @@ class HasCTE(roles.HasCTERole): return CTE._construct(self, name=name, recursive=recursive) -class FromGrouping(FromClause): - """Represent a grouping of a FROM clause""" +class Subquery(AliasedReturnsRows): + __visit_name__ = "subquery" - __visit_name__ = "grouping" + _is_subquery = True + + @classmethod + def _factory(cls, selectable, name=None): + """Return a :class:`.Subquery` object. + + """ + return coercions.expect( + roles.SelectStatementRole, selectable + ).subquery(name=name) + + +class FromGrouping(GroupedElement, FromClause): + """Represent a grouping of a FROM clause""" def __init__(self, element): - self.element = element + self.element = coercions.expect(roles.FromClauseRole, element) def _init_collections(self): pass @@ -1794,9 +1768,6 @@ class FromGrouping(FromClause): def _from_objects(self): return self.element._from_objects - def __getattr__(self, attr): - return getattr(self.element, attr) - def __getstate__(self): return {"element": self.element} @@ -1804,7 +1775,7 @@ class FromGrouping(FromClause): self.element = state["element"] -class TableClause(roles.AnonymizedFromClauseRole, Immutable, FromClause): +class TableClause(Immutable, FromClause): """Represents a minimal "table" construct. This is a lightweight table object that has only a name and a @@ -1870,6 +1841,9 @@ class TableClause(roles.AnonymizedFromClauseRole, Immutable, FromClause): for c in columns: self.append_column(c) + def _refresh_for_new_column(self, column): + pass + def _init_collections(self): pass @@ -2065,19 +2039,122 @@ class SelectBase( roles.InElementRole, HasCTE, Executable, - FromClause, + Selectable, ): """Base class for SELECT statements. This includes :class:`.Select`, :class:`.CompoundSelect` and - :class:`.TextAsFrom`. + :class:`.TextualSelect`. """ _is_select_statement = True + _memoized_property = util.group_expirable_memoized_property() + + def _reset_memoizations(self): + self._memoized_property.expire_instance(self) + + def _generate_fromclause_column_proxies(self, fromclause): + # type: (FromClause) + raise NotImplementedError() + + def _refresh_for_new_column(self, column): + self._reset_memoizations() + + @property + def selected_columns(self): + """A :class:`.ColumnCollection` representing the columns that + this SELECT statement or similar construct returns in its result set. + + This collection differs from the :attr:`.FromClause.columns` collection + of a :class:`.FromClause` in that the columns within this collection + cannot be directly nested inside another SELECT statement; a subquery + must be applied first which provides for the necessary parenthesization + required by SQL. + + .. versionadded:: 1.4 + + """ + raise NotImplementedError() + + @property + def exported_columns(self): + """A :class:`.ColumnCollection` that represents the "exported" + columns of this :class:`.Selectable`. + + The "exported" columns for a :class:`.SelectBase` object are synonymous + with the :attr:`.SelectBase.selected_columns` collection. + + .. versionadded:: 1.4 + + .. seealso: + + :attr:`.Selectable.exported_columns` + + :attr:`.FromClause.exported_columns` + + + """ + return self.selected_columns + + @property + @util.deprecated( + "1.4", + "The :attr:`.SelectBase.c` and :attr:`.SelectBase.columns` attributes " + "are deprecated and will be removed in a future release; these " + "attributes implicitly create a subquery that should be explicit. " + "Please call :meth:`.SelectBase.subquery` first in order to create " + "a subquery, which then contains this attribute. To access the " + "columns that this SELECT object SELECTs " + "from, use the :attr:`.SelectBase.selected_columns` attribute.", + ) + def c(self): + return self._implicit_subquery.columns + + @property + def columns(self): + return self.c + + @util.deprecated( + "1.4", + "The :meth:`.SelectBase.select` method is deprecated " + "and will be removed in a future release; this method implicitly " + "creates a subquery that should be explicit. " + "Please call :meth:`.SelectBase.subquery` first in order to create " + "a subquery, which then can be seleted.", + ) + def select(self, *arg, **kw): + return self._implicit_subquery.select(*arg, **kw) + + @util.deprecated( + "1.4", + "The :meth:`.SelectBase.join` method is deprecated " + "and will be removed in a future release; this method implicitly " + "creates a subquery that should be explicit. " + "Please call :meth:`.SelectBase.subquery` first in order to create " + "a subquery, which then can be seleted.", + ) + def join(self, *arg, **kw): + return self._implicit_subquery.join(*arg, **kw) + + @util.deprecated( + "1.4", + "The :meth:`.SelectBase.outerjoin` method is deprecated " + "and will be removed in a future release; this method implicitly " + "creates a subquery that should be explicit. " + "Please call :meth:`.SelectBase.subquery` first in order to create " + "a subquery, which then can be seleted.", + ) + def outerjoin(self, *arg, **kw): + return self._implicit_subquery.outerjoin(*arg, **kw) + + @_memoized_property + def _implicit_subquery(self): + return self.subquery() + @util.deprecated( "1.4", "The :meth:`.SelectBase.as_scalar` method is deprecated and will be " @@ -2117,6 +2194,21 @@ class SelectBase( """ return self.scalar_subquery().label(name) + def lateral(self, name=None): + """Return a LATERAL alias of this :class:`.Selectable`. + + The return value is the :class:`.Lateral` construct also + provided by the top-level :func:`~.expression.lateral` function. + + .. versionadded:: 1.1 + + .. seealso:: + + :ref:`lateral_selects` - overview of usage. + + """ + return Lateral._factory(self, name) + @_generative @util.deprecated( "0.6", @@ -2141,7 +2233,7 @@ class SelectBase( s = self.__class__.__new__(self.__class__) s.__dict__ = self.__dict__.copy() - s._reset_exported() + s._reset_memoizations() return s @property @@ -2182,7 +2274,78 @@ class SelectBase( .. versionadded:: 1.4 """ - return self.alias() + return Subquery._construct(self, name) + + def alias(self, name=None, flat=False): + """Return a named subquery against this :class:`.SelectBase`. + + For a :class:`.SelectBase` (as opposed to a :class:`.FromClause`), + this returns a :class:`.Subquery` object which behaves mostly the + same as the :class:`.Alias` object that is used with a + :class:`.FromClause`. + + .. versionchanged:: 1.4 The :meth:`.SelectBase.alias` method is now + a synonym for the :meth:`.SelectBase.subquery` method. + + """ + return self.subquery(name=name) + + +class SelectStatementGrouping(GroupedElement, SelectBase): + """Represent a grouping of a :class:`.SelectBase`. + + This differs from :class:`.Subquery` in that we are still + an "inner" SELECT statement, this is strictly for grouping inside of + compound selects. + + """ + + __visit_name__ = "grouping" + + def __init__(self, element): + # type: (SelectBase) + self.element = coercions.expect(roles.SelectStatementRole, element) + + @property + def select_statement(self): + return self.element + + def get_children(self, **kwargs): + return (self.element,) + + def self_group(self, against=None): + # type: (Optional[Any]) -> FromClause + return self + + def _generate_fromclause_column_proxies(self, subquery): + self.element._generate_fromclause_column_proxies(subquery) + + def _generate_proxy_for_new_column(self, column, subquery): + return self.element._generate_proxy_for_new_column(subquery) + + @property + def selected_columns(self): + """A :class:`.ColumnCollection` representing the columns that + the embedded SELECT statement returns in its result set. + + .. versionadded:: 1.4 + + .. seealso:: + + :ref:`.SelectBase.selected_columns` + + """ + return self.element.selected_columns + + def _copy_internals(self, clone=_clone, **kw): + self.element = clone(self.element, **kw) + + def _cache_key(self, **kw): + return (SelectStatementGrouping, self.element._cache_key(**kw)) + + @property + def _from_objects(self): + return self.element._from_objects class GenerativeSelect(SelectBase): @@ -2191,7 +2354,7 @@ class GenerativeSelect(SelectBase): This serves as the base for :class:`.Select` and :class:`.CompoundSelect` where elements such as ORDER BY, GROUP BY can be added and column - rendering can be controlled. Compare to :class:`.TextAsFrom`, which, + rendering can be controlled. Compare to :class:`.TextualSelect`, which, while it subclasses :class:`.SelectBase` and is also a SELECT construct, represents a fixed textual string which cannot be altered at this level, only wrapped as a subquery. @@ -2199,7 +2362,7 @@ class GenerativeSelect(SelectBase): .. versionadded:: 0.9.0 :class:`.GenerativeSelect` was added to provide functionality specific to :class:`.Select` and :class:`.CompoundSelect` while allowing :class:`.SelectBase` to be - used for other SELECT-like objects, e.g. :class:`.TextAsFrom`. + used for other SELECT-like objects, e.g. :class:`.TextualSelect`. """ @@ -2591,8 +2754,8 @@ class CompoundSelect(GenerativeSelect): s = coercions.expect(roles.CompoundElementRole, s) if not numcols: - numcols = len(s.c._all_columns) - elif len(s.c._all_columns) != numcols: + numcols = len(s.selected_columns) + elif len(s.selected_columns) != numcols: raise exc.ArgumentError( "All selectables passed to " "CompoundSelect must have identical numbers of " @@ -2600,9 +2763,9 @@ class CompoundSelect(GenerativeSelect): "#%d has %d" % ( 1, - len(self.selects[0].c._all_columns), + len(self.selects[0].selected_columns), n + 1, - len(s.c._all_columns), + len(s.selected_columns), ) ) @@ -2610,9 +2773,12 @@ class CompoundSelect(GenerativeSelect): GenerativeSelect.__init__(self, **kwargs) - @property + @SelectBase._memoized_property def _label_resolve_dict(self): - d = dict((c.key, c) for c in self.c) + # 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 @@ -2727,7 +2893,8 @@ class CompoundSelect(GenerativeSelect): return self.selects[0]._scalar_type() def self_group(self, against=None): - return FromGrouping(self) + # type: (Optional[Any]) -> FromClause + return SelectStatementGrouping(self) def is_derived_from(self, fromclause): for s in self.selects: @@ -2735,50 +2902,59 @@ class CompoundSelect(GenerativeSelect): return True return False - def _populate_column_collection(self): - for cols in zip(*[s.c._all_columns for s in self.selects]): - - # this is a slightly hacky thing - the union exports a - # column that resembles just that of the *first* selectable. - # to get at a "composite" column, particularly foreign keys, - # you have to dig through the proxies collection which we - # generate below. We may want to improve upon this, such as - # perhaps _make_proxy can accept a list of other columns - # that are "shared" - schema.column can then copy all the - # ForeignKeys in. this would allow the union() to have all - # those fks too. - - proxy = cols[0]._make_proxy( - self, - name=cols[0]._label if self.use_labels else None, - key=cols[0]._key_label if self.use_labels else None, - ) - - # hand-construct the "_proxies" collection to include all - # derived columns place a 'weight' annotation corresponding - # to how low in the list of select()s the column occurs, so - # that the corresponding_column() operation can resolve - # conflicts - proxy._proxies = [ - c._annotate({"weight": i + 1}) for (i, c) in enumerate(cols) + def _generate_fromclause_column_proxies(self, subquery): + + # this is a slightly hacky thing - the union exports a + # column that resembles just that of the *first* selectable. + # to get at a "composite" column, particularly foreign keys, + # you have to dig through the proxies collection which we + # generate below. We may want to improve upon this, such as + # perhaps _make_proxy can accept a list of other columns + # that are "shared" - schema.column can then copy all the + # ForeignKeys in. this would allow the union() to have all + # those fks too. + select_0 = self.selects[0] + if self.use_labels: + select_0 = select_0.apply_labels() + select_0._generate_fromclause_column_proxies(subquery) + + # hand-construct the "_proxies" collection to include all + # derived columns place a 'weight' annotation corresponding + # to how low in the list of select()s the column occurs, so + # that the corresponding_column() operation can resolve + # conflicts + for subq_col, select_cols in zip( + subquery.c._all_columns, + zip(*[s.selected_columns for s in self.selects]), + ): + subq_col._proxies = [ + c._annotate({"weight": i + 1}) + for (i, c) in enumerate(select_cols) ] def _refresh_for_new_column(self, column): - for s in self.selects: - s._refresh_for_new_column(column) + super(CompoundSelect, self)._refresh_for_new_column(column) + for select in self.selects: + select._refresh_for_new_column(column) - if not self._cols_populated: - return None + @property + def selected_columns(self): + """A :class:`.ColumnCollection` representing the columns that + this SELECT statement or similar construct returns in its result set. - raise NotImplementedError( - "CompoundSelect constructs don't support " - "addition of columns to underlying " - "selectables" - ) + For a :class:`.CompoundSelect`, the + :attr:`.CompoundSelect.selected_columns` attribute returns the selected + columns of the first SELECT statement contined within the series of + statements within the set operation. + + .. versionadded:: 1.4 + + """ + return self.selects[0].selected_columns def _copy_internals(self, clone=_clone, **kw): super(CompoundSelect, self)._copy_internals(clone, **kw) - self._reset_exported() + self._reset_memoizations() self.selects = [clone(s, **kw) for s in self.selects] if hasattr(self, "_col_map"): del self._col_map @@ -2790,11 +2966,9 @@ class CompoundSelect(GenerativeSelect): if getattr(self, attr) is not None: setattr(self, attr, clone(getattr(self, attr), **kw)) - def get_children(self, column_collections=True, **kwargs): - return ( - (column_collections and list(self.c) or []) - + [self._order_by_clause, self._group_by_clause] - + list(self.selects) + def get_children(self, **kwargs): + return [self._order_by_clause, self._group_by_clause] + list( + self.selects ) def _cache_key(self, **kw): @@ -3142,7 +3316,7 @@ class Select(HasPrefixes, HasSuffixes, GenerativeSelect): else (), self._from_obj, ): - if item is self: + if item._is_subquery and item.element is self: raise exc.InvalidRequestError( "select() construct refers to itself as a FROM" ) @@ -3415,16 +3589,15 @@ class Select(HasPrefixes, HasSuffixes, GenerativeSelect): if getattr(self, attr) is not None: setattr(self, attr, clone(getattr(self, attr), **kw)) - # erase exported column list, _froms collection, + # erase _froms collection, # etc. - self._reset_exported() + self._reset_memoizations() - def get_children(self, column_collections=True, **kwargs): + def get_children(self, **kwargs): """return child elements as per the ClauseElement specification.""" return ( - (column_collections and list(self.columns) or []) - + self._raw_columns + self._raw_columns + list(self._froms) + [ x @@ -3594,7 +3767,7 @@ class Select(HasPrefixes, HasSuffixes, GenerativeSelect): asked to select both from ``table1`` as well as itself. """ - self._reset_exported() + self._reset_memoizations() rc = [] for c in columns: c = coercions.expect(roles.ColumnsClauseRole, c) @@ -3789,7 +3962,7 @@ class Select(HasPrefixes, HasSuffixes, GenerativeSelect): :class:`.Select` object. """ - self._reset_exported() + self._reset_memoizations() column = coercions.expect(roles.ColumnsClauseRole, column) if isinstance(column, ScalarSelect): @@ -3821,7 +3994,7 @@ class Select(HasPrefixes, HasSuffixes, GenerativeSelect): """ - self._reset_exported() + self._reset_memoizations() self._whereclause = and_(True_._ifnone(self._whereclause), whereclause) def append_having(self, having): @@ -3835,7 +4008,7 @@ class Select(HasPrefixes, HasSuffixes, GenerativeSelect): :term:`method chaining`. """ - self._reset_exported() + self._reset_memoizations() self._having = and_(True_._ifnone(self._having), having) def append_from(self, fromclause): @@ -3847,11 +4020,61 @@ class Select(HasPrefixes, HasSuffixes, GenerativeSelect): standard :term:`method chaining`. """ - self._reset_exported() + self._reset_memoizations() fromclause = coercions.expect(roles.FromClauseRole, fromclause) self._from_obj = self._from_obj.union([fromclause]) @_memoized_property + def selected_columns(self): + """A :class:`.ColumnCollection` representing the columns that + this SELECT statement or similar construct returns in its result set. + + This collection differs from the :attr:`.FromClause.columns` collection + of a :class:`.FromClause` in that the columns within this collection + cannot be directly nested inside another SELECT statement; a subquery + must be applied first which provides for the necessary parenthesization + required by SQL. + + For a :func:`.select` construct, the collection here is exactly what + would be rendered inside the "SELECT" statement, and the + :class:`.ColumnElement` objects are directly present as they were + given, e.g.:: + + col1 = column('q', Integer) + col2 = column('p', Integer) + stmt = select([col1, col2]) + + Above, ``stmt.selected_columns`` would be a collection that contains + the ``col1`` and ``col2`` objects directly. For a statement that is + against a :class:`.Table` or other :class:`.FromClause`, the collection + will use the :class:`.ColumnElement` objects that are in the + :attr:`.FromClause.c` collection of the from element. + + .. versionadded:: 1.4 + + """ + names = set() + + def name_for_col(c): + # we use key_label since this name is intended for targeting + # within the ColumnCollection only, it's not related to SQL + # rendering which always uses column name for SQL label names + if self.use_labels: + name = c._key_label + else: + name = c._proxy_key + if name in names: + name = c.anon_label + else: + names.add(name) + return name + + return SeparateKeyColumnCollection( + (name_for_col(c), c) + for c in util.unique_list(_select_iterables(self._raw_columns)) + ).as_immutable() + + @_memoized_property def _columns_plus_names(self): if self.use_labels: names = set() @@ -3877,7 +4100,9 @@ class Select(HasPrefixes, HasSuffixes, GenerativeSelect): for c in util.unique_list(_select_iterables(self._raw_columns)) ] - def _populate_column_collection(self): + def _generate_fromclause_column_proxies(self, subquery): + keys_seen = set() + for name, c in self._columns_plus_names: if not hasattr(c, "_make_proxy"): continue @@ -3885,27 +4110,15 @@ class Select(HasPrefixes, HasSuffixes, GenerativeSelect): key = None elif self.use_labels: key = c._key_label - if key is not None and key in self.c: + if key is not None and key in keys_seen: key = c.anon_label + keys_seen.add(key) else: key = None - c._make_proxy(self, key=key, name=name, name_is_truncatable=True) - def _refresh_for_new_column(self, column): - for fromclause in self._froms: - col = fromclause._refresh_for_new_column(column) - if col is not None: - if col in self.inner_columns and self._cols_populated: - our_label = col._key_label if self.use_labels else col.key - if our_label not in self.c: - return col._make_proxy( - self, - name=col._label if self.use_labels else None, - key=col._key_label if self.use_labels else None, - name_is_truncatable=True, - ) - return None - return None + c._make_proxy( + subquery, key=key, name=name, name_is_truncatable=True + ) def _needs_parens_for_grouping(self): return ( @@ -3928,7 +4141,8 @@ class Select(HasPrefixes, HasSuffixes, GenerativeSelect): and not self._needs_parens_for_grouping() ): return self - return FromGrouping(self) + else: + return SelectStatementGrouping(self) def union(self, other, **kwargs): """return a SQL UNION of this select() construct against the given @@ -4030,7 +4244,6 @@ class Exists(UnaryExpression): """ - __visit_name__ = UnaryExpression.__visit_name__ _from_objects = [] def __init__(self, *args, **kwargs): @@ -4056,7 +4269,7 @@ class Exists(UnaryExpression): else: if not args: args = ([literal_column("*")],) - s = Select(*args, **kwargs).scalar_subquery().self_group() + s = Select(*args, **kwargs).scalar_subquery() UnaryExpression.__init__( self, @@ -4066,17 +4279,26 @@ class Exists(UnaryExpression): wraps_column_expression=True, ) + def _regroup(self, fn): + element = self.element._ungroup() + element = fn(element) + return element.self_group(against=operators.exists) + def select(self, whereclause=None, **params): return Select([self], whereclause, **params) def correlate(self, *fromclause): e = self._clone() - e.element = self.element.correlate(*fromclause).self_group() + e.element = self._regroup( + lambda element: element.correlate(*fromclause) + ) return e def correlate_except(self, *fromclause): e = self._clone() - e.element = self.element.correlate_except(*fromclause).self_group() + e.element = self._regroup( + lambda element: element.correlate_except(*fromclause) + ) return e def select_from(self, clause): @@ -4086,7 +4308,7 @@ class Exists(UnaryExpression): """ e = self._clone() - e.element = self.element.select_from(clause).self_group() + e.element = self._regroup(lambda element: element.select_from(clause)) return e def where(self, clause): @@ -4095,12 +4317,11 @@ class Exists(UnaryExpression): """ e = self._clone() - e.element = self.element.where(clause).self_group() + e.element = self._regroup(lambda element: element.where(clause)) return e -# TODO: rename to TextualSelect, this is not a FROM clause -class TextAsFrom(SelectBase): +class TextualSelect(SelectBase): """Wrap a :class:`.TextClause` construct within a :class:`.SelectBase` interface. @@ -4108,20 +4329,22 @@ class TextAsFrom(SelectBase): and other FROM-like capabilities such as :meth:`.FromClause.alias`, :meth:`.SelectBase.cte`, etc. - The :class:`.TextAsFrom` construct is produced via the + The :class:`.TextualSelect` construct is produced via the :meth:`.TextClause.columns` method - see that method for details. - .. versionadded:: 0.9.0 + .. versionchanged:: 1.4 the :class:`.TextualSelect` class was renamed + from ``TextAsFrom``, to more correctly suit its role as a + SELECT-oriented object and not a FROM clause. .. seealso:: :func:`.text` - :meth:`.TextClause.columns` + :meth:`.TextClause.columns` - primary creation interface. """ - __visit_name__ = "text_as_from" + __visit_name__ = "textual_select" _is_textual = True @@ -4130,6 +4353,26 @@ class TextAsFrom(SelectBase): self.column_args = columns self.positional = positional + @SelectBase._memoized_property + def selected_columns(self): + """A :class:`.ColumnCollection` representing the columns that + this SELECT statement or similar construct returns in its result set. + + This collection differs from the :attr:`.FromClause.columns` collection + of a :class:`.FromClause` in that the columns within this collection + cannot be directly nested inside another SELECT statement; a subquery + must be applied first which provides for the necessary parenthesization + required by SQL. + + For a :class:`.TextualSelect` construct, the collection contains the + :class:`.ColumnElement` objects that were passed to the constructor, + typically via the :meth:`.TextClause.columns` method. + + .. versionadded:: 1.4 + + """ + return ColumnCollection(*self.column_args).as_immutable() + @property def _bind(self): return self.element._bind @@ -4138,22 +4381,19 @@ class TextAsFrom(SelectBase): def bindparams(self, *binds, **bind_as_values): self.element = self.element.bindparams(*binds, **bind_as_values) - def _populate_column_collection(self): + def _generate_fromclause_column_proxies(self, fromclause): for c in self.column_args: - c._make_proxy(self) + c._make_proxy(fromclause) def _copy_internals(self, clone=_clone, **kw): - self._reset_exported() + self._reset_memoizations() self.element = clone(self.element, **kw) - def get_children(self, column_collections=True, **kw): - if column_collections: - for c in self.column_args: - yield c - yield self.element + def get_children(self, **kw): + return [self.element] def _cache_key(self, **kw): - return (TextAsFrom, self.element._cache_key(**kw)) + tuple( + return (TextualSelect, self.element._cache_key(**kw)) + tuple( col._cache_key(**kw) for col in self.column_args ) @@ -4161,6 +4401,10 @@ class TextAsFrom(SelectBase): return self.column_args[0].type +TextAsFrom = TextualSelect +"""Backwards compatibility with the previous name""" + + class AnnotatedFromClause(Annotated): def __init__(self, element, values): # force FromClause to generate their internal diff --git a/lib/sqlalchemy/sql/util.py b/lib/sqlalchemy/sql/util.py index d90b3f158..dd2c7c1fb 100644 --- a/lib/sqlalchemy/sql/util.py +++ b/lib/sqlalchemy/sql/util.py @@ -17,10 +17,10 @@ from . import visitors from .annotation import _deep_annotate # noqa from .annotation import _deep_deannotate # noqa from .annotation import _shallow_annotate # noqa +from .base import _expand_cloned from .base import _from_objects from .base import ColumnSet from .ddl import sort_tables # noqa -from .elements import _expand_cloned from .elements import _find_columns # noqa from .elements import _label_reference from .elements import _textual_label_reference |