From 485216dea6d7a5814d200b4f14b8a363ed0f8caa Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 1 Oct 2019 17:38:41 -0400 Subject: Deprecate textual column matching in Row Deprecate query.instances() without a context Deprecate string alias with contains_eager() Deprecated the behavior by which a :class:`.Column` can be used as the key in a result set row lookup, when that :class:`.Column` is not part of the SQL selectable that is being selected; that is, it is only matched on name. A deprecation warning is now emitted for this case. Various ORM use cases, such as those involving :func:`.text` constructs, have been improved so that this fallback logic is avoided in most cases. Calling the :meth:`.Query.instances` method without passing a :class:`.QueryContext` is deprecated. The original use case for this was that a :class:`.Query` could yield ORM objects when given only the entities to be selected as well as a DBAPI cursor object. However, for this to work correctly there is essential metadata that is passed from a SQLAlchemy :class:`.ResultProxy` that is derived from the mapped column expressions, which comes originally from the :class:`.QueryContext`. To retrieve ORM results from arbitrary SELECT statements, the :meth:`.Query.from_statement` method should be used. Note there is a small bump in test_zoomark because the column._label is being calculated for each of those columns within baseline_3_properties, as it is now part of the result map. This label can't be calculated when the column is attached to the table because it needs to have all the columns present to do this correctly. Another approach here would be to pre-load the _label before the test runs however the zoomark tests don't have an easy place for this to happen and it's not really worth it. Fixes: #4877 Fixes: #4719 Change-Id: I9bd29e72e6dce7c855651d69ba68d7383469acbc --- lib/sqlalchemy/sql/compiler.py | 29 ++++++++++++++++++++++++++--- lib/sqlalchemy/sql/elements.py | 9 +++++++-- lib/sqlalchemy/sql/selectable.py | 5 ++++- lib/sqlalchemy/sql/util.py | 4 +--- 4 files changed, 38 insertions(+), 9 deletions(-) (limited to 'lib/sqlalchemy/sql') diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 5e432a74c..1381e734c 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -490,6 +490,13 @@ class SQLCompiler(Compiled): True unless using an unordered TextualSelect. """ + _loose_column_name_matching = False + """tell the result object that the SQL staement is textual, wants to match + up to Column objects, and may be using the ._label in the SELECT rather + than the base name. + + """ + _numeric_binds = False """ True if paramstyle is "numeric". This paramstyle is trickier than @@ -799,6 +806,7 @@ class SQLCompiler(Compiled): within_label_clause=False, within_columns_clause=False, render_label_as_label=None, + result_map_targets=(), **kw ): # only render labels within the columns clause @@ -820,7 +828,7 @@ class SQLCompiler(Compiled): add_to_result_map( labelname, label.name, - (label, labelname) + label._alt_names, + (label, labelname) + label._alt_names + result_map_targets, label.type, ) @@ -847,7 +855,12 @@ class SQLCompiler(Compiled): ) def visit_column( - self, column, add_to_result_map=None, include_table=True, **kwargs + self, + column, + add_to_result_map=None, + include_table=True, + result_map_targets=(), + **kwargs ): name = orig_name = column.name if name is None: @@ -859,7 +872,10 @@ class SQLCompiler(Compiled): if add_to_result_map is not None: add_to_result_map( - name, orig_name, (column, name, column.key), column.type + name, + orig_name, + (column, name, column.key, column._label) + result_map_targets, + column.type, ) if is_literal: @@ -948,6 +964,13 @@ class SQLCompiler(Compiled): self._ordered_columns = ( self._textual_ordered_columns ) = taf.positional + + # enable looser result column matching when the SQL text links to + # Column objects by name only + self._loose_column_name_matching = not taf.positional and bool( + taf.column_args + ) + for c in taf.column_args: self.process( c, diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index 3045cb84e..8ee157b6f 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -4278,7 +4278,13 @@ class ColumnClause(roles.LabeledColumnExprRole, Immutable, ColumnElement): label = quoted_name(label, t.name.quote) # ensure the label name doesn't conflict with that - # of an existing column + # of an existing column. note that this implies that any + # Column must **not** set up its _label before its parent table + # has all of its other Column objects set up. There are several + # tables in the test suite which will fail otherwise; example: + # table "owner" has columns "name" and "owner_name". Therefore + # column owner.name cannot use the label "owner_name", it has + # to be "owner_name_1". if label in t.c: _label = label counter = 1 @@ -4339,7 +4345,6 @@ class ColumnClause(roles.LabeledColumnExprRole, Immutable, ColumnElement): c._proxies = [self] if selectable._is_clone_of is not None: c._is_clone_of = selectable._is_clone_of.columns.get(c.key) - return c.key, c diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py index b41a77622..33ba95717 100644 --- a/lib/sqlalchemy/sql/selectable.py +++ b/lib/sqlalchemy/sql/selectable.py @@ -4449,7 +4449,10 @@ class TextualSelect(SelectBase): def __init__(self, text, columns, positional=False): self.element = text - self.column_args = columns + # convert for ORM attributes->columns, etc + self.column_args = [ + coercions.expect(roles.ColumnsClauseRole, c) for c in columns + ] self.positional = positional @SelectBase._memoized_property diff --git a/lib/sqlalchemy/sql/util.py b/lib/sqlalchemy/sql/util.py index 780cdc7b2..5aeed0c1c 100644 --- a/lib/sqlalchemy/sql/util.py +++ b/lib/sqlalchemy/sql/util.py @@ -798,9 +798,7 @@ class ClauseAdapter(visitors.ReplacingCloningVisitor): if newcol is not None: return newcol if self.adapt_on_names and newcol is None: - # TODO: this should be changed to .exported_columns if and - # when we need to be able to adapt a plain Select statement - newcol = self.selectable.c.get(col.name) + newcol = self.selectable.exported_columns.get(col.name) return newcol def replace(self, col): -- cgit v1.2.1