diff options
author | Gord Thompson <gord@gordthompson.com> | 2021-01-09 14:56:38 -0700 |
---|---|---|
committer | Mike Bayer <mike_mp@zzzcomputing.com> | 2021-01-26 16:52:30 -0500 |
commit | 22f65156bbe846dea2fb9f87fe4187abe0ed790a (patch) | |
tree | f49338a10dd2800d4d754b14d2e7fd549b4b833f /lib/sqlalchemy/sql/selectable.py | |
parent | 7bdb1f30f66aaea16efbcf96e314491058493e6c (diff) | |
download | sqlalchemy-22f65156bbe846dea2fb9f87fe4187abe0ed790a.tar.gz |
Replace with_labels() and apply_labels() in ORM/Core
Replace :meth:`_orm.Query.with_labels` and
:meth:`_sql.GenerativeSelect.apply_labels` with explicit getters and
setters ``get_label_style`` and ``set_label_style`` to accommodate the
three supported label styles: ``LABEL_STYLE_DISAMBIGUATE_ONLY`` (default),
``LABEL_STYLE_TABLENAME_PLUS_COL``, and ``LABEL_STYLE_NONE``.
In addition, for Core and "future style" ORM queries,
``LABEL_STYLE_DISAMBIGUATE_ONLY`` is now the default label style. This
style differs from the existing "no labels" style in that labeling is
applied in the case of column name conflicts; with ``LABEL_STYLE_NONE``, a
duplicate column name is not accessible via name in any case.
For legacy ORM queries using :class:`_query.Query`, the table-plus-column
names labeling style applied by ``LABEL_STYLE_TABLENAME_PLUS_COL``
continues to be used so that existing test suites and logging facilities
see no change in behavior by default, however this style of labeling is no
longer required for SQLAlchemy queries to function, as result sets are
commonly matched to columns using a positional approach since SQLAlchemy
1.0.
Within test suites, all use of apply_labels() / use_labels
now uses the new methods. New tests added to
test/sql/test_deprecations.py nad test/orm/test_deprecations.py
to cover just the old apply_labels() method call. Tests
in ORM that made explicit use apply_labels()/ etc. where it isn't needed
for the ORM to work correctly use default label style now.
Co-authored-by: Mike Bayer <mike_mp@zzzcomputing.com>
Fixes: #4757
Change-Id: I5fdcd2ed4ae8c7fe62f8be2b6d0e8f66409b6a54
Diffstat (limited to 'lib/sqlalchemy/sql/selectable.py')
-rw-r--r-- | lib/sqlalchemy/sql/selectable.py | 208 |
1 files changed, 176 insertions, 32 deletions
diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py index 8e478583f..e1cf367c8 100644 --- a/lib/sqlalchemy/sql/selectable.py +++ b/lib/sqlalchemy/sql/selectable.py @@ -825,7 +825,7 @@ class FromClause(roles.AnonymizedFromClauseRole, Selectable): this is used to "ping" a derived selectable to add a new column to its .c. collection when a Column has been added to one of the - Table objects it ultimtely derives from. + Table objects it ultimately derives from. If the given selectable hasn't populated its .c. collection yet, it should at least pass on the message to the contained selectables, @@ -849,6 +849,96 @@ class FromClause(roles.AnonymizedFromClauseRole, Selectable): return self.alias(name=name) +LABEL_STYLE_NONE = util.symbol( + "LABEL_STYLE_NONE", + """Label style indicating no automatic labeling should be applied to the + columns clause of a SELECT statement. + + Below, the columns named ``columna`` are both rendered as is, meaning that + the name ``columna`` can only refer to the first occurrence of this name + within a result set, as well as if the statement were used as a subquery:: + + >>> from sqlalchemy import table, column, select, true, LABEL_STYLE_NONE + >>> table1 = table("table1", column("columna"), column("columnb")) + >>> table2 = table("table2", column("columna"), column("columnc")) + >>> print(select(table1, table2).join(table2, true()).set_label_style(LABEL_STYLE_NONE)) + SELECT table1.columna, table1.columnb, table2.columna, table2.columnc + FROM table1 JOIN table2 ON true + + Used with the :meth:`_sql.Select.set_label_style` method. + + .. versionadded:: 1.4 + +""", # noqa E501 +) + +LABEL_STYLE_TABLENAME_PLUS_COL = util.symbol( + "LABEL_STYLE_TABLENAME_PLUS_COL", + """Label style indicating all columns should be labeled as + ``<tablename>_<columnname>`` when generating the columns clause of a SELECT + statement, to disambiguate same-named columns referenced from different + tables, aliases, or subqueries. + + Below, all column names are given a label so that the two same-named + columns ``columna`` are disambiguated as ``table1_columna`` and + ``table2_columna`:: + + >>> from sqlalchemy import table, column, select, true, LABEL_STYLE_TABLENAME_PLUS_COL + >>> table1 = table("table1", column("columna"), column("columnb")) + >>> table2 = table("table2", column("columna"), column("columnc")) + >>> print(select(table1, table2).join(table2, true()).set_label_style(LABEL_STYLE_TABLENAME_PLUS_COL)) + SELECT table1.columna AS table1_columna, table1.columnb AS table1_columnb, table2.columna AS table2_columna, table2.columnc AS table2_columnc + FROM table1 JOIN table2 ON true + + Used with the :meth:`_sql.GenerativeSelect.set_label_style` method. + Equivalent to the legacy method ``Select.apply_labels()``; + :data:`_sql.LABEL_STYLE_TABLENAME_PLUS_COL` is SQLAlchemy's legacy + auto-labeling style. :data:`_sql.LABEL_STYLE_DISAMBIGUATE_ONLY` provides a + less intrusive approach to disambiguation of same-named column expressions. + + + .. versionadded:: 1.4 + +""", # noqa E501 +) + + +LABEL_STYLE_DISAMBIGUATE_ONLY = util.symbol( + "LABEL_STYLE_DISAMBIGUATE_ONLY", + """Label style indicating that columns with a name that conflicts with + an existing name should be labeled with a semi-anonymizing label + when generating the columns clause of a SELECT statement. + + Below, most column names are left unaffected, except for the second + occurrence of the name ``columna``, which is labeled using the + label ``columna_1`` to disambiguate it from that of ``tablea.columna``:: + + >>> from sqlalchemy import table, column, select, true, LABEL_STYLE_DISAMBIGUATE_ONLY + >>> table1 = table("table1", column("columna"), column("columnb")) + >>> table2 = table("table2", column("columna"), column("columnc")) + >>> print(select(table1, table2).join(table2, true()).set_label_style(LABEL_STYLE_DISAMBIGUATE_ONLY)) + SELECT table1.columna, table1.columnb, table2.columna AS columna_1, table2.columnc + FROM table1 JOIN table2 ON true + + Used with the :meth:`_sql.GenerativeSelect.set_label_style` method, + :data:`_sql.LABEL_STYLE_DISAMBIGUATE_ONLY` is the default labeling style + for all SELECT statements outside of :term:`1.x style` ORM queries. + + .. versionadded:: 1.4 + +""", # noqa: E501, +) + + +LABEL_STYLE_DEFAULT = LABEL_STYLE_DISAMBIGUATE_ONLY +"""The default label style, refers to +:data:`_sql.LABEL_STYLE_DISAMBIGUATE_ONLY`. + +.. versionadded:: 1.4 + +""" + + class Join(roles.DMLTableRole, FromClause): """Represent a ``JOIN`` construct between two :class:`_expression.FromClause` @@ -1277,7 +1367,12 @@ class Join(roles.DMLTableRole, FromClause): full=self.full, ) else: - return self.select().apply_labels().correlate(None).alias(name) + return ( + self.select() + .set_label_style(LABEL_STYLE_TABLENAME_PLUS_COL) + .correlate(None) + .alias(name) + ) @util.deprecated_20( ":meth:`_sql.Join.alias`", @@ -1312,7 +1407,7 @@ class Join(roles.DMLTableRole, FromClause): j = alias( select(j.left, j.right).\ select_from(j).\ - apply_labels().\ + set_label_style(LABEL_STYLE_TABLENAME_PLUS_COL).\ correlate(False), name=name ) @@ -2054,9 +2149,7 @@ class Subquery(AliasedReturnsRows): "use the :meth:`_query.Query.scalar_subquery` method.", ) def as_scalar(self): - return self.element._set_label_style( - LABEL_STYLE_NONE - ).scalar_subquery() + return self.element.set_label_style(LABEL_STYLE_NONE).scalar_subquery() class FromGrouping(GroupedElement, FromClause): @@ -2633,7 +2726,7 @@ class SelectBase( """ if self._label_style is not LABEL_STYLE_NONE: - self = self._set_label_style(LABEL_STYLE_NONE) + self = self.set_label_style(LABEL_STYLE_NONE) return ScalarSelect(self) @@ -2760,6 +2853,14 @@ class SelectStatementGrouping(GroupedElement, SelectBase): else: return self + def get_label_style(self): + return self._label_style + + def set_label_style(self, label_style): + return SelectStatementGrouping( + self.element.set_label_style(label_style) + ) + @property def _label_style(self): return self.element._label_style @@ -2854,11 +2955,6 @@ class DeprecatedSelectBaseGenerations(object): self.group_by.non_generative(self, *clauses) -LABEL_STYLE_NONE = util.symbol("LABEL_STYLE_NONE") -LABEL_STYLE_TABLENAME_PLUS_COL = util.symbol("LABEL_STYLE_TABLENAME_PLUS_COL") -LABEL_STYLE_DISAMBIGUATE_ONLY = util.symbol("LABEL_STYLE_DISAMBIGUATE_ONLY") - - class GenerativeSelect(DeprecatedSelectBaseGenerations, SelectBase): """Base class for SELECT statements where additional elements can be added. @@ -2892,7 +2988,7 @@ class GenerativeSelect(DeprecatedSelectBaseGenerations, SelectBase): ) def __init__( self, - _label_style=LABEL_STYLE_NONE, + _label_style=LABEL_STYLE_DEFAULT, use_labels=False, limit=None, offset=None, @@ -2901,6 +2997,15 @@ class GenerativeSelect(DeprecatedSelectBaseGenerations, SelectBase): bind=None, ): if use_labels: + if util.SQLALCHEMY_WARN_20: + util.warn_deprecated_20( + "The use_labels=True keyword argument to GenerativeSelect " + "is deprecated and will be removed in version 2.0. Please " + "use " + "select.set_label_style(LABEL_STYLE_TABLENAME_PLUS_COL) " + "if you need to replicate this legacy behavior.", + stacklevel=4, + ) _label_style = LABEL_STYLE_TABLENAME_PLUS_COL self._label_style = _label_style @@ -2979,29 +3084,66 @@ class GenerativeSelect(DeprecatedSelectBaseGenerations, SelectBase): key_share=key_share, ) - @property - def use_labels(self): - return self._label_style is LABEL_STYLE_TABLENAME_PLUS_COL + def get_label_style(self): + """ + Retrieve the current label style. - def apply_labels(self): - """Return a new selectable with the 'use_labels' flag set to True. + .. versionadded:: 1.4 - This will result in column expressions being generated using labels - against their table name, such as "SELECT somecolumn AS - tablename_somecolumn". This allows selectables which contain multiple - FROM clauses to produce a unique set of column names regardless of - name conflicts among the individual FROM clauses. + """ + return self._label_style + + def set_label_style(self, style): + """Return a new selectable with the specified label style. + + There are three "label styles" available, + :data:`_sql.LABEL_STYLE_DISAMBIGUATE_ONLY`, + :data:`_sql.LABEL_STYLE_TABLENAME_PLUS_COL`, and + :data:`_sql.LABEL_STYLE_NONE`. The default style is + :data:`_sql.LABEL_STYLE_TABLENAME_PLUS_COL`. + + In modern SQLAlchemy, there is not generally a need to change the + labeling style, as per-expression labels are more effectively used by + making use of the :meth:`_sql.ColumnElement.label` method. In past + versions, :data:`_sql.LABEL_STYLE_TABLENAME_PLUS_COL` was used to + disambiguate same-named columns from different tables, aliases, or + subqueries; the newer :data:`_sql.LABEL_STYLE_DISAMBIGUATE_ONLY` now + applies labels only to names that conflict with an existing name so + that the impact of this labeling is minimal. + + The rationale for disambiguation is mostly so that all column + expressions are available from a given :attr:`_sql.FromClause.c` + collection when a subquery is created. + + .. versionadded:: 1.4 - the + :meth:`_sql.GenerativeSelect.set_label_style` method replaces the + previous combination of ``.apply_labels()``, ``.with_labels()`` and + ``use_labels=True`` methods and/or parameters. + .. seealso:: - """ - return self._set_label_style(LABEL_STYLE_TABLENAME_PLUS_COL) + :data:`_sql.LABEL_STYLE_DISAMBIGUATE_ONLY` - def _set_label_style(self, style): + :data:`_sql.LABEL_STYLE_TABLENAME_PLUS_COL` + + :data:`_sql.LABEL_STYLE_NONE` + + :data:`_sql.LABEL_STYLE_DEFAULT` + + """ if self._label_style is not style: self = self._generate() self._label_style = style return self + @util.deprecated_20( + ":meth:`_sql.GenerativeSelect.apply_labels`", + alternative="Use set_label_style(LABEL_STYLE_TABLENAME_PLUS_COL) " + "instead.", + ) + def apply_labels(self): + return self.set_label_style(LABEL_STYLE_TABLENAME_PLUS_COL) + @property def _group_by_clause(self): """ClauseList access to group_by_clauses for legacy dialects""" @@ -3525,8 +3667,9 @@ class CompoundSelect(HasCompileState, GenerativeSelect): # ForeignKeys in. this would allow the union() to have all # those fks too. select_0 = self.selects[0] - if self._label_style is not LABEL_STYLE_NONE: - select_0 = select_0._set_label_style(self._label_style) + + if self._label_style is not LABEL_STYLE_DEFAULT: + select_0 = select_0.set_label_style(self._label_style) select_0._generate_fromclause_column_proxies(subquery) # hand-construct the "_proxies" collection to include all @@ -4347,12 +4490,12 @@ class Select( This parameter can also be specified on an existing :class:`_expression.Select` object using the - :meth:`_expression.Select.apply_labels` + :meth:`_expression.Select.set_label_style` method. .. seealso:: - :meth:`_expression.Select.apply_labels` + :meth:`_expression.Select.set_label_style` """ self = cls.__new__(cls) @@ -4917,7 +5060,8 @@ class Select( comparison in the WHERE clause of the statement. The primary purpose of this method is to automatically construct a select statement with all uniquely-named columns, without the need to use - table-qualified labels as :meth:`_expression.Select.apply_labels` + table-qualified labels as + :meth:`_expression.Select.set_label_style` does. When columns are omitted based on foreign key, the referred-to @@ -5263,7 +5407,7 @@ class Select( def _ensure_disambiguated_names(self): if self._label_style is LABEL_STYLE_NONE: - self = self._set_label_style(LABEL_STYLE_DISAMBIGUATE_ONLY) + self = self.set_label_style(LABEL_STYLE_DISAMBIGUATE_ONLY) return self def _generate_columns_plus_names(self, anon_for_dupe_key): |