From 2cf8c5868cb83185001755d86aa0f79e0318b53f Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 22 Jun 2021 13:27:18 -0400 Subject: Export deferred columns but not col props; fix CTE labeling Refined the behavior of ORM subquery rendering with regards to deferred columns and column properties to be more compatible with that of 1.3 while also providing for 1.4's newer features. As a subquery in 1.4 does not make use of loader options, including :func:`_orm.deferred`, a subquery that is against an ORM entity with deferred attributes will now render those deferred attributes that refer directly to mapped table columns, as these are needed in the outer SELECT if that outer SELECT makes use of these columns; however a deferred attribute that refers to a composed SQL expression as we normally do with :func:`_orm.column_property` will not be part of the subquery, as these can be selected explicitly if needed in the subquery. If the entity is being SELECTed from this subquery, the column expression can still render on "the outside" in terms of the derived subquery columns. This produces essentially the same behavior as when working with 1.3. However in this case the fix has to also make sure that the ``.selected_columns`` collection of an ORM-enabled :func:`_sql.select` also follows these rules, which in particular allows recursive CTEs to render correctly in this scenario, which were previously failing to render correctly due to this issue. As part of this change the _exported_columns_iterator() method has been removed and logic simplified to use ._all_selected_columns from any SelectBase object where _exported_columns_iterator() was used before. Additionally sets up UpdateBase to include ReturnsRows in its hierarchy; the literal point of ReturnsRows was to be a common base for UpdateBase and SelectBase so it was kind of weird it wasn't there. Fixes: #6661 Fixed issue in CTE constructs mostly relevant to ORM use cases where a recursive CTE against "anonymous" labels such as those seen in ORM ``column_property()`` mappings would render in the ``WITH RECURSIVE xyz(...)`` section as their raw internal label and not a cleanly anonymized name. Fixes: #6663 Change-Id: I26219d4d8e6c0915b641426e9885540f74fae4d2 --- lib/sqlalchemy/sql/compiler.py | 15 ++++++++-- lib/sqlalchemy/sql/dml.py | 13 ++++----- lib/sqlalchemy/sql/selectable.py | 62 ++++++++++++++-------------------------- 3 files changed, 39 insertions(+), 51 deletions(-) (limited to 'lib/sqlalchemy/sql') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 220a0fa99..f47ea8f33 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -2562,18 +2562,20 @@ class SQLCompiler(Compiled): elif isinstance(cte.element, selectable.CompoundSelect): col_source = cte.element.selects[0] else: - assert False + assert False, "cte should only be against SelectBase" recur_cols = [ c for c in util.unique_list( - col_source._exported_columns_iterator() + col_source._all_selected_columns ) if c is not None ] text += "(%s)" % ( ", ".join( - self.preparer.format_column(ident) + self.preparer.format_column( + ident, anon_map=self.anon_map + ) for ident in recur_cols ) ) @@ -5012,11 +5014,18 @@ class IdentifierPreparer(object): name=None, table_name=None, use_schema=False, + anon_map=None, ): """Prepare a quoted column name.""" if name is None: name = column.name + + if anon_map is not None and isinstance( + name, elements._truncated_label + ): + name = name.apply_map(anon_map) + if not getattr(column, "is_literal", False): if use_table: return ( diff --git a/lib/sqlalchemy/sql/dml.py b/lib/sqlalchemy/sql/dml.py index ea10bfc27..a6ef62619 100644 --- a/lib/sqlalchemy/sql/dml.py +++ b/lib/sqlalchemy/sql/dml.py @@ -27,6 +27,7 @@ from .elements import ClauseElement from .elements import Null from .selectable import HasCTE from .selectable import HasPrefixes +from .selectable import ReturnsRows from .visitors import InternalTraversal from .. import exc from .. import util @@ -199,6 +200,7 @@ class UpdateBase( HasCompileState, DialectKWArgs, HasPrefixes, + ReturnsRows, Executable, ClauseElement, ): @@ -415,13 +417,8 @@ class UpdateBase( coercions.expect(roles.ColumnsClauseRole, c) for c in cols ) - def _exported_columns_iterator(self): - """Return the RETURNING columns as a sequence for this statement. - - .. versionadded:: 1.4 - - """ - + @property + def _all_selected_columns(self): return self._returning @property @@ -434,7 +431,7 @@ class UpdateBase( """ # TODO: no coverage here return ColumnCollection( - (c.key, c) for c in self._exported_columns_iterator() + (c.key, c) for c in self._all_selected_columns ).as_immutable() @_generative diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py index e1dee091b..557c443bf 100644 --- a/lib/sqlalchemy/sql/selectable.py +++ b/lib/sqlalchemy/sql/selectable.py @@ -109,23 +109,19 @@ class ReturnsRows(roles.ReturnsRowsRole, ClauseElement): @property def selectable(self): - raise NotImplementedError() - - def _exported_columns_iterator(self): - """An iterator of column objects that represents the "exported" - columns of this :class:`_expression.ReturnsRows`. + return self - This is the same set of columns as are returned by - :meth:`_expression.ReturnsRows.exported_columns` - except they are returned - as a simple iterator or sequence, rather than as a - :class:`_expression.ColumnCollection` namespace. + @property + def _all_selected_columns(self): + """A sequence of column expression objects that represents the + "selected" columns of this :class:`_expression.ReturnsRows`. - Subclasses should re-implement this method to bypass the interim - creation of the :class:`_expression.ColumnCollection` if appropriate. + This is typically equivalent to .exported_columns except it is + delivered in the form of a straight sequence and not keyed + :class:`_expression.ColumnCollection`. """ - return iter(self.exported_columns) + raise NotImplementedError() @property def exported_columns(self): @@ -161,10 +157,6 @@ class Selectable(ReturnsRows): is_selectable = True - @property - def selectable(self): - return self - def _refresh_for_new_column(self, column): raise NotImplementedError() @@ -3113,9 +3105,6 @@ class SelectStatementGrouping(GroupedElement, SelectBase): def _generate_proxy_for_new_column(self, column, subquery): return self.element._generate_proxy_for_new_column(subquery) - def _exported_columns_iterator(self): - return self.element._exported_columns_iterator() - @property def _all_selected_columns(self): return self.element._all_selected_columns @@ -3935,9 +3924,6 @@ class CompoundSelect(HasCompileState, GenerativeSelect): for select in self.selects: select._refresh_for_new_column(column) - def _exported_columns_iterator(self): - return self.selects[0]._exported_columns_iterator() - @property def _all_selected_columns(self): return self.selects[0]._all_selected_columns @@ -4335,7 +4321,7 @@ class SelectState(util.MemoizedSlots, CompileState): def _memoized_attr__label_resolve_dict(self): with_cols = dict( (c._resolve_label or c._label or c.key, c) - for c in self.statement._exported_columns_iterator() + for c in self.statement._all_selected_columns if c._allow_label_resolve ) only_froms = dict( @@ -4356,14 +4342,6 @@ class SelectState(util.MemoizedSlots, CompileState): else: return None - @classmethod - def exported_columns_iterator(cls, statement): - return [ - c - for c in _select_iterables(statement._raw_columns) - if not c._is_text_clause - ] - @classmethod def all_selected_columns(cls, statement): return [c for c in _select_iterables(statement._raw_columns)] @@ -5318,7 +5296,7 @@ class Select( """ - return self._exported_columns_iterator() + return iter(self._all_selected_columns) def is_derived_from(self, fromclause): if self in fromclause._cloned_set: @@ -5470,7 +5448,7 @@ class Select( """ return self.with_only_columns( *util.preloaded.sql_util.reduce_columns( - self._exported_columns_iterator(), + self._all_selected_columns, only_synonyms=only_synonyms, *(self._where_criteria + self._from_obj) ) @@ -5779,7 +5757,11 @@ class Select( conv = SelectState._column_naming_convention(self._label_style) return ColumnCollection( - [(conv(c), c) for c in self._exported_columns_iterator()] + [ + (conv(c), c) + for c in self._all_selected_columns + if not c._is_text_clause + ] ).as_immutable() @HasMemoized.memoized_attribute @@ -5787,10 +5769,6 @@ class Select( meth = SelectState.get_plugin_class(self).all_selected_columns return list(meth(self)) - def _exported_columns_iterator(self): - meth = SelectState.get_plugin_class(self).exported_columns_iterator - return meth(self) - def _ensure_disambiguated_names(self): if self._label_style is LABEL_STYLE_NONE: self = self.set_label_style(LABEL_STYLE_DISAMBIGUATE_ONLY) @@ -5912,7 +5890,7 @@ class Select( disambiguate_only = self._label_style is LABEL_STYLE_DISAMBIGUATE_ONLY for name, c, repeated in self._generate_columns_plus_names(False): - if not hasattr(c, "_make_proxy"): + if c._is_text_clause: continue elif tablename_plus_col: key = c._key_label @@ -6405,6 +6383,10 @@ class TextualSelect(SelectBase): (c.key, c) for c in self.column_args ).as_immutable() + @property + def _all_selected_columns(self): + return self.column_args + def _set_label_style(self, style): return self -- cgit v1.2.1