diff options
| author | mike bayer <mike_mp@zzzcomputing.com> | 2021-07-13 14:25:13 +0000 |
|---|---|---|
| committer | Gerrit Code Review <gerrit@ci3.zzzcomputing.com> | 2021-07-13 14:25:13 +0000 |
| commit | 673ca806b323f47ef7064dd64ffc98240818b930 (patch) | |
| tree | 56fbe5fde5a3892475949ce16e84793364bf0eb2 /lib/sqlalchemy | |
| parent | 326e3f5dc26f22ca51f4aa2509856d8077e76dec (diff) | |
| parent | 707e5d70fcdcfaaddcd0aaee51f4f1b881e5e3e2 (diff) | |
| download | sqlalchemy-673ca806b323f47ef7064dd64ffc98240818b930.tar.gz | |
Merge "labeling refactor"
Diffstat (limited to 'lib/sqlalchemy')
| -rw-r--r-- | lib/sqlalchemy/dialects/firebird/base.py | 2 | ||||
| -rw-r--r-- | lib/sqlalchemy/dialects/mssql/base.py | 6 | ||||
| -rw-r--r-- | lib/sqlalchemy/dialects/postgresql/base.py | 2 | ||||
| -rw-r--r-- | lib/sqlalchemy/engine/cursor.py | 12 | ||||
| -rw-r--r-- | lib/sqlalchemy/orm/mapper.py | 2 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/compiler.py | 176 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/elements.py | 243 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/functions.py | 8 | ||||
| -rw-r--r-- | lib/sqlalchemy/sql/selectable.py | 263 |
9 files changed, 478 insertions, 236 deletions
diff --git a/lib/sqlalchemy/dialects/firebird/base.py b/lib/sqlalchemy/dialects/firebird/base.py index 61e3e4508..91e2c04a7 100644 --- a/lib/sqlalchemy/dialects/firebird/base.py +++ b/lib/sqlalchemy/dialects/firebird/base.py @@ -539,7 +539,7 @@ class FBCompiler(sql.compiler.SQLCompiler): def returning_clause(self, stmt, returning_cols): columns = [ - self._label_select_column(None, c, True, False, {}) + self._label_returning_column(stmt, c) for c in expression._select_iterables(returning_cols) ] diff --git a/lib/sqlalchemy/dialects/mssql/base.py b/lib/sqlalchemy/dialects/mssql/base.py index 67d31226c..c11166735 100644 --- a/lib/sqlalchemy/dialects/mssql/base.py +++ b/lib/sqlalchemy/dialects/mssql/base.py @@ -2010,11 +2010,9 @@ class MSSQLCompiler(compiler.SQLCompiler): # necessarily used an expensive KeyError in order to match. columns = [ - self._label_select_column( - None, + self._label_returning_column( + stmt, adapter.traverse(c), - True, - False, {"result_map_targets": (c,)}, ) for c in expression._select_iterables(returning_cols) diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index 76fc09f9d..070490c1d 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -2305,7 +2305,7 @@ class PGCompiler(compiler.SQLCompiler): def returning_clause(self, stmt, returning_cols): columns = [ - self._label_select_column(None, c, True, False, {}) + self._label_returning_column(stmt, c) for c in expression._select_iterables(returning_cols) ] diff --git a/lib/sqlalchemy/engine/cursor.py b/lib/sqlalchemy/engine/cursor.py index 09c6a4db7..5e6078f86 100644 --- a/lib/sqlalchemy/engine/cursor.py +++ b/lib/sqlalchemy/engine/cursor.py @@ -728,12 +728,18 @@ class LegacyCursorResultMetaData(CursorResultMetaData): result = map_.get(key if self.case_sensitive else key.lower()) elif isinstance(key, expression.ColumnElement): if ( - key._label - and (key._label if self.case_sensitive else key._label.lower()) + key._tq_label + and ( + key._tq_label + if self.case_sensitive + else key._tq_label.lower() + ) in map_ ): result = map_[ - key._label if self.case_sensitive else key._label.lower() + key._tq_label + if self.case_sensitive + else key._tq_label.lower() ] elif ( hasattr(key, "name") diff --git a/lib/sqlalchemy/orm/mapper.py b/lib/sqlalchemy/orm/mapper.py index d691b8e1d..530c0a112 100644 --- a/lib/sqlalchemy/orm/mapper.py +++ b/lib/sqlalchemy/orm/mapper.py @@ -1729,7 +1729,7 @@ class Mapper( if isinstance(col, expression.Label): # new in 1.4, get column property against expressions # to be addressable in subqueries - col.key = col._key_label = key + col.key = col._tq_key_label = key self.columns.add(col, key) for col in prop.columns + prop._orig_columns: diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index e31a3839e..7007c2e86 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -35,7 +35,6 @@ from . import crud from . import elements from . import functions from . import operators -from . import roles from . import schema from . import selectable from . import sqltypes @@ -612,7 +611,7 @@ class SQLCompiler(Compiled): _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 + up to Column objects, and may be using the ._tq_label in the SELECT rather than the base name. """ @@ -1457,8 +1456,8 @@ class SQLCompiler(Compiled): if add_to_result_map is not None: targets = (column, name, column.key) + result_map_targets - if column._label: - targets += (column._label,) + if column._tq_label: + targets += (column._tq_label,) add_to_result_map(name, orig_name, targets, column.type) @@ -2816,6 +2815,24 @@ class SQLCompiler(Compiled): ) self._result_columns.append((keyname, name, objects, type_)) + def _label_returning_column(self, stmt, column, column_clause_args=None): + """Render a column with necessary labels inside of a RETURNING clause. + + This method is provided for individual dialects in place of calling + the _label_select_column method directly, so that the two use cases + of RETURNING vs. SELECT can be disambiguated going forward. + + .. versionadded:: 1.4.21 + + """ + return self._label_select_column( + None, + column, + True, + False, + {} if column_clause_args is None else column_clause_args, + ) + def _label_select_column( self, select, @@ -2824,6 +2841,8 @@ class SQLCompiler(Compiled): asfrom, column_clause_args, name=None, + proxy_name=None, + fallback_label_name=None, within_columns_clause=True, column_is_repeated=False, need_column_expressions=False, @@ -2867,9 +2886,17 @@ class SQLCompiler(Compiled): else: add_to_result_map = None - if not within_columns_clause: - result_expr = col_expr - elif isinstance(column, elements.Label): + # this method is used by some of the dialects for RETURNING, + # which has different inputs. _label_returning_column was added + # as the better target for this now however for 1.4 we will keep + # _label_select_column directly compatible with this use case. + # these assertions right now set up the current expected inputs + assert within_columns_clause, ( + "_label_select_column is only relevant within " + "the columns clause of a SELECT or RETURNING" + ) + + if isinstance(column, elements.Label): if col_expr is not column: result_expr = _CompileLabel( col_expr, column.name, alt_names=(column.element,) @@ -2877,50 +2904,91 @@ class SQLCompiler(Compiled): else: result_expr = col_expr - elif select is not None and name: - result_expr = _CompileLabel( - col_expr, name, alt_names=(column._key_label,) - ) - elif ( - asfrom - and isinstance(column, elements.ColumnClause) - and not column.is_literal - and column.table is not None - and not isinstance(column.table, selectable.Select) - ): - result_expr = _CompileLabel( - col_expr, - coercions.expect(roles.TruncatedLabelRole, column.name), - alt_names=(column.key,), - ) - elif ( - not isinstance(column, elements.TextClause) - and ( - not isinstance(column, elements.UnaryExpression) - or column.wraps_column_expression - or asfrom - ) - and ( - not hasattr(column, "name") - or isinstance(column, functions.FunctionElement) - ) - ): - result_expr = _CompileLabel( - col_expr, - column._anon_name_label - if not column_is_repeated - else column._dedupe_label_anon_label, - ) - elif col_expr is not column: - # TODO: are we sure "column" has a .name and .key here ? - # assert isinstance(column, elements.ColumnClause) + elif name: + # here, _columns_plus_names has determined there's an explicit + # label name we need to use. this is the default for + # tablenames_plus_columnnames as well as when columns are being + # deduplicated on name + + assert ( + proxy_name is not None + ), "proxy_name is required if 'name' is passed" + result_expr = _CompileLabel( col_expr, - coercions.expect(roles.TruncatedLabelRole, column.name), - alt_names=(column.key,), + name, + alt_names=( + proxy_name, + # this is a hack to allow legacy result column lookups + # to work as they did before; this goes away in 2.0. + # TODO: this only seems to be tested indirectly + # via test/orm/test_deprecations.py. should be a + # resultset test for this + column._tq_label, + ), ) else: - result_expr = col_expr + # determine here whether this column should be rendered in + # a labelled context or not, as we were given no required label + # name from the caller. Here we apply heuristics based on the kind + # of SQL expression involved. + + if col_expr is not column: + # type-specific expression wrapping the given column, + # so we render a label + render_with_label = True + elif isinstance(column, elements.ColumnClause): + # table-bound column, we render its name as a label if we are + # inside of a subquery only + render_with_label = ( + asfrom + and not column.is_literal + and column.table is not None + ) + elif isinstance(column, elements.TextClause): + render_with_label = False + elif isinstance(column, elements.UnaryExpression): + render_with_label = column.wraps_column_expression or asfrom + elif ( + # general class of expressions that don't have a SQL-column + # addressible name. includes scalar selects, bind parameters, + # SQL functions, others + not isinstance(column, elements.NamedColumn) + # deeper check that indicates there's no natural "name" to + # this element, which accommodates for custom SQL constructs + # that might have a ".name" attribute (but aren't SQL + # functions) but are not implementing this more recently added + # base class. in theory the "NamedColumn" check should be + # enough, however here we seek to maintain legacy behaviors + # as well. + and column._non_anon_label is None + ): + render_with_label = True + else: + render_with_label = False + + if render_with_label: + if not fallback_label_name: + # used by the RETURNING case right now. we generate it + # here as 3rd party dialects may be referring to + # _label_select_column method directly instead of the + # just-added _label_returning_column method + assert not column_is_repeated + fallback_label_name = column._anon_name_label + + fallback_label_name = ( + elements._truncated_label(fallback_label_name) + if not isinstance( + fallback_label_name, elements._truncated_label + ) + else fallback_label_name + ) + + result_expr = _CompileLabel( + col_expr, fallback_label_name, alt_names=(proxy_name,) + ) + else: + result_expr = col_expr column_clause_args.update( within_columns_clause=within_columns_clause, @@ -3096,10 +3164,18 @@ class SQLCompiler(Compiled): asfrom, column_clause_args, name=name, + proxy_name=proxy_name, + fallback_label_name=fallback_label_name, column_is_repeated=repeated, need_column_expressions=need_column_expressions, ) - for name, column, repeated in compile_state.columns_plus_names + for ( + name, + proxy_name, + fallback_label_name, + column, + repeated, + ) in compile_state.columns_plus_names ] if c is not None ] @@ -3114,6 +3190,8 @@ class SQLCompiler(Compiled): name for ( key, + proxy_name, + fallback_label_name, name, repeated, ) in compile_state.columns_plus_names @@ -3122,6 +3200,8 @@ class SQLCompiler(Compiled): name for ( key, + proxy_name, + fallback_label_name, name, repeated, ) in compile_state_wraps_for.columns_plus_names diff --git a/lib/sqlalchemy/sql/elements.py b/lib/sqlalchemy/sql/elements.py index 173314abe..f95fa143e 100644 --- a/lib/sqlalchemy/sql/elements.py +++ b/lib/sqlalchemy/sql/elements.py @@ -687,19 +687,21 @@ class ColumnElement( foreign_keys = [] _proxies = () - _label = None + _tq_label = None """The named label that can be used to target - this column in a result set. + this column in a result set in a "table qualified" context. This label is almost always the label used when - rendering <expr> AS <label> in a SELECT statement. It also - refers to a name that this column expression can be located from - in a result set. + rendering <expr> AS <label> in a SELECT statement when using + the LABEL_STYLE_TABLENAME_PLUS_COL label style, which is what the legacy + ORM ``Query`` object uses as well. For a regular Column bound to a Table, this is typically the label <tablename>_<columnname>. For other constructs, different rules may apply, such as anonymized labels and others. + .. versionchanged:: 1.4.21 renamed from ``._label`` + """ key = None @@ -712,19 +714,69 @@ class ColumnElement( """ - _key_label = None - """A label-based version of 'key' that in some circumstances refers - to this object in a Python namespace. + @HasMemoized.memoized_attribute + def _tq_key_label(self): + """A label-based version of 'key' that in some circumstances refers + to this object in a Python namespace. - _key_label comes into play when a select() statement is constructed with - apply_labels(); in this case, all Column objects in the ``.c`` collection - are rendered as <tablename>_<columnname> in SQL; this is essentially the - value of ._label. But to locate those columns in the ``.c`` collection, - the name is along the lines of <tablename>_<key>; that's the typical - value of .key_label. + _tq_key_label comes into play when a select() statement is constructed + with apply_labels(); in this case, all Column objects in the ``.c`` + collection are rendered as <tablename>_<columnname> in SQL; this is + essentially the value of ._label. But to locate those columns in the + ``.c`` collection, the name is along the lines of <tablename>_<key>; + that's the typical value of .key_label. - """ + .. versionchanged:: 1.4.21 renamed from ``._key_label`` + + """ + return self._proxy_key + + @property + def _key_label(self): + """legacy; renamed to _tq_key_label""" + return self._tq_key_label + + @property + def _label(self): + """legacy; renamed to _tq_label""" + return self._tq_label + + @property + def _non_anon_label(self): + """the 'name' that naturally applies this element when rendered in + SQL. + + Concretely, this is the "name" of a column or a label in a + SELECT statement; ``<columnname>`` and ``<labelname>`` below:: + + SELECT <columnmame> FROM table + + SELECT column AS <labelname> FROM table + + Above, the two names noted will be what's present in the DBAPI + ``cursor.description`` as the names. + + If this attribute returns ``None``, it means that the SQL element as + written does not have a 100% fully predictable "name" that would appear + in the ``cursor.description``. Examples include SQL functions, CAST + functions, etc. While such things do return names in + ``cursor.description``, they are only predictable on a + database-specific basis; e.g. an expression like ``MAX(table.col)`` may + appear as the string ``max`` on one database (like PostgreSQL) or may + appear as the whole expression ``max(table.col)`` on SQLite. + + The default implementation looks for a ``.name`` attribute on the + object, as has been the precedent established in SQLAlchemy for many + years. An exception is made on the ``FunctionElement`` subclass + so that the return value is always ``None``. + + .. versionadded:: 1.4.21 + + + + """ + return getattr(self, "name", None) _render_label_in_columns_clause = True """A flag used by select._columns_plus_names that helps to determine @@ -878,21 +930,33 @@ class ColumnElement( and other.name == self.name ) - @util.memoized_property + @HasMemoized.memoized_attribute def _proxy_key(self): if self._annotations and "proxy_key" in self._annotations: return self._annotations["proxy_key"] - elif self.key: - return self.key + + name = self.key + if not name: + # there's a bit of a seeming contradiction which is that the + # "_non_anon_label" of a column can in fact be an + # "_anonymous_label"; this is when it's on a column that is + # proxying for an anonymous expression in a subquery. + name = self._non_anon_label + + if isinstance(name, _anonymous_label): + return None else: - return getattr(self, "name", "_no_label") + return name - @util.memoized_property + @HasMemoized.memoized_attribute def _expression_label(self): """a suggested label to use in the case that the column has no name, which should be used if possible as the explicit 'AS <label>' where this expression would normally have an anon label. + this is essentially mostly what _proxy_key does except it returns + None if the column has a normal name that can be used. + """ if getattr(self, "name", None) is not None: @@ -1031,20 +1095,39 @@ class ColumnElement( @util.memoized_property def _dedupe_anon_label(self): - label = getattr(self, "name", None) or "anon" - return self._anon_label(label + "_") + """label to apply to a column that is anon labeled, but repeated + in the SELECT, so that we have to make an "extra anon" label that + disambiguates it from the previous appearance. + + these labels come out like "foo_bar_id__1" and have double underscores + in them. + + """ + label = getattr(self, "name", None) + + # current convention is that if the element doesn't have a + # ".name" (usually because it is not NamedColumn), we try to + # use a "table qualified" form for the "dedupe anon" label, + # based on the notion that a label like + # "CAST(casttest.v1 AS DECIMAL) AS casttest_v1__1" looks better than + # "CAST(casttest.v1 AS DECIMAL) AS anon__1" + + if label is None: + return self._dedupe_anon_tq_label + else: + return self._anon_label(label + "_") @util.memoized_property - def _label_anon_label(self): - return self._anon_label(getattr(self, "_label", None)) + def _anon_tq_label(self): + return self._anon_label(getattr(self, "_tq_label", None)) @util.memoized_property - def _label_anon_key_label(self): - return self._anon_label(getattr(self, "_key_label", None)) + def _anon_tq_key_label(self): + return self._anon_label(getattr(self, "_tq_key_label", None)) @util.memoized_property - def _dedupe_label_anon_label(self): - label = getattr(self, "_label", None) or "anon" + def _dedupe_anon_tq_label(self): + label = getattr(self, "_tq_label", None) or "anon" return self._anon_label(label + "_") @@ -1067,22 +1150,42 @@ class WrapsColumnExpression(object): raise NotImplementedError() @property - def _label(self): + def _tq_label(self): wce = self.wrapped_column_expression - if hasattr(wce, "_label"): - return wce._label + if hasattr(wce, "_tq_label"): + return wce._tq_label else: return None + _label = _tq_label + + @property + def _non_anon_label(self): + return None + @property def _anon_name_label(self): wce = self.wrapped_column_expression - if hasattr(wce, "name"): - return wce.name - elif hasattr(wce, "_anon_name_label"): - return wce._anon_name_label + + # this logic tries to get the WrappedColumnExpression to render + # with "<expr> AS <name>", where "<name>" is the natural name + # within the expression itself. e.g. "CAST(table.foo) AS foo". + if not wce._is_text_clause: + nal = wce._non_anon_label + if nal: + return nal + elif hasattr(wce, "_anon_name_label"): + return wce._anon_name_label + return super(WrapsColumnExpression, self)._anon_name_label + + @property + def _dedupe_anon_label(self): + wce = self.wrapped_column_expression + nal = wce._non_anon_label + if nal: + return self._anon_label(nal + "_") else: - return super(WrapsColumnExpression, self)._anon_name_label + return self._dedupe_anon_tq_label class BindParameter(roles.InElementRole, ColumnElement): @@ -3812,12 +3915,10 @@ class Grouping(GroupedElement, ColumnElement): return self.element._is_implicitly_boolean @property - def _key_label(self): - return self._label - - @property - def _label(self): - return getattr(self.element, "_label", None) or self._anon_name_label + def _tq_label(self): + return ( + getattr(self.element, "_tq_label", None) or self._anon_name_label + ) @property def _proxies(self): @@ -4330,7 +4431,7 @@ class Label(roles.LabeledColumnExprRole, ColumnElement): id(self), getattr(element, "name", "anon") ) - self.key = self._label = self._key_label = self.name + self.key = self._tq_label = self._tq_key_label = self.name self._element = element self._type = type_ self._proxies = [element] @@ -4388,7 +4489,7 @@ class Label(roles.LabeledColumnExprRole, ColumnElement): self.name = self._resolve_label = _anonymous_label.safe_construct( id(self), getattr(self.element, "name", "anon") ) - self.key = self._label = self._key_label = self.name + self.key = self._tq_label = self._tq_key_label = self.name @property def _from_objects(self): @@ -4444,22 +4545,39 @@ class NamedColumn(ColumnElement): return self.name.encode("ascii", "backslashreplace") @HasMemoized.memoized_attribute - def _key_label(self): + def _tq_key_label(self): + """table qualified label based on column key. + + for table-bound columns this is <tablename>_<column key/proxy key>; + + all other expressions it resolves to key/proxy key. + + """ proxy_key = self._proxy_key - if proxy_key != self.name: - return self._gen_label(proxy_key) + if proxy_key and proxy_key != self.name: + return self._gen_tq_label(proxy_key) else: - return self._label + return self._tq_label @HasMemoized.memoized_attribute - def _label(self): - return self._gen_label(self.name) + def _tq_label(self): + """table qualified label based on column name. + + for table-bound columns this is <tablename>_<columnname>; all other + expressions it resolves to .name. + + """ + return self._gen_tq_label(self.name) @HasMemoized.memoized_attribute def _render_label_in_columns_clause(self): return True - def _gen_label(self, name, dedupe_on_key=True): + @HasMemoized.memoized_attribute + def _non_anon_label(self): + return self.name + + def _gen_tq_label(self, name, dedupe_on_key=True): return name def _bind_param(self, operator, obj, type_=None, expanding=False): @@ -4682,7 +4800,7 @@ class ColumnClause( @property def _ddl_label(self): - return self._gen_label(self.name, dedupe_on_key=False) + return self._gen_tq_label(self.name, dedupe_on_key=False) def _compare_name_for_result(self, other): if ( @@ -4700,12 +4818,21 @@ class ColumnClause( ) ): return (hasattr(other, "name") and self.name == other.name) or ( - hasattr(other, "_label") and self._label == other._label + hasattr(other, "_tq_label") + and self._tq_label == other._tq_label ) else: return other.proxy_set.intersection(self.proxy_set) - def _gen_label(self, name, dedupe_on_key=True): + def _gen_tq_label(self, name, dedupe_on_key=True): + """generate table-qualified label + + for a table-bound column this is <tablename>_<columnname>. + + used primarily for LABEL_STYLE_TABLENAME_PLUS_COL + as well as the .columns collection on a Join object. + + """ t = self.table if self.is_literal: return None @@ -4967,7 +5094,13 @@ def _corresponding_column_or_error(fromclause, column, require_embedded=False): class AnnotatedColumnElement(Annotated): def __init__(self, element, values): Annotated.__init__(self, element, values) - for attr in ("comparator", "_proxy_key", "_key_label"): + for attr in ( + "comparator", + "_proxy_key", + "_tq_key_label", + "_tq_label", + "_non_anon_label", + ): self.__dict__.pop(attr, None) for attr in ("name", "key", "table"): if self.__dict__.get(attr, False) is None: diff --git a/lib/sqlalchemy/sql/functions.py b/lib/sqlalchemy/sql/functions.py index dd807210f..900bc6dba 100644 --- a/lib/sqlalchemy/sql/functions.py +++ b/lib/sqlalchemy/sql/functions.py @@ -125,6 +125,14 @@ class FunctionElement(Executable, ColumnElement, FromClause, Generative): operator=operators.comma_op, group_contents=True, *args ).self_group() + _non_anon_label = None + + @property + def _proxy_key(self): + return super(FunctionElement, self)._proxy_key or getattr( + self, "name", None + ) + def _execute_on_connection( self, connection, multiparams, params, execution_options ): diff --git a/lib/sqlalchemy/sql/selectable.py b/lib/sqlalchemy/sql/selectable.py index d947aed37..30a613089 100644 --- a/lib/sqlalchemy/sql/selectable.py +++ b/lib/sqlalchemy/sql/selectable.py @@ -1132,7 +1132,7 @@ class Join(roles.DMLTableRole, FromClause): ) ) self._columns._populate_separate_keys( - (col._key_label, col) for col in columns + (col._tq_key_label, col) for col in columns ) self.foreign_keys.update( itertools.chain(*[col.foreign_keys for col in columns]) @@ -4222,49 +4222,41 @@ class SelectState(util.MemoizedSlots, CompileState): @classmethod def _column_naming_convention(cls, label_style): - # note: these functions won't work for TextClause objects, - # which should be omitted when iterating through - # _raw_columns. - if label_style is LABEL_STYLE_NONE: + table_qualified = label_style is LABEL_STYLE_TABLENAME_PLUS_COL + dedupe = label_style is not LABEL_STYLE_NONE - def go(c, col_name=None): - return c._proxy_key + pa = prefix_anon_map() + names = set() - elif label_style is LABEL_STYLE_TABLENAME_PLUS_COL: - names = set() - pa = [] # late-constructed as needed, python 2 has no "nonlocal" - - def go(c, col_name=None): - # 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 - - name = c._key_label - - if name in names: - if not pa: - pa.append(prefix_anon_map()) - - name = c._label_anon_key_label % pa[0] - else: - names.add(name) + def go(c, col_name=None): + if c._is_text_clause: + return None + elif not dedupe: + name = c._proxy_key + if name is None: + name = "_no_label" return name - else: - names = set() - pa = [] # late-constructed as needed, python 2 has no "nonlocal" + name = c._tq_key_label if table_qualified else c._proxy_key - def go(c, col_name=None): - name = c._proxy_key + if name is None: + name = "_no_label" if name in names: - if not pa: - pa.append(prefix_anon_map()) - name = c._anon_key_label % pa[0] + return c._anon_label(name) % pa else: names.add(name) + return name + elif name in names: + return ( + c._anon_tq_key_label % pa + if table_qualified + else c._anon_key_label % pa + ) + else: + names.add(name) return name return go @@ -4394,7 +4386,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) + (c._resolve_label or c._tq_label or c.key, c) for c in self.statement._all_selected_columns if c._allow_label_resolve ) @@ -5853,60 +5845,70 @@ class Select( """Generate column names as rendered in a SELECT statement by the compiler. - This is distinct from other name generators that are intended for - population of .c collections and similar, which may have slightly - different rules. + This is distinct from the _column_naming_convention generator that's + intended for population of .c collections and similar, which has + different rules. the collection returned here calls upon the + _column_naming_convention as well. """ cols = self._all_selected_columns - # when use_labels is on: - # in all cases == if we see the same label name, use _label_anon_label - # for subsequent occurrences of that label - # - # anon_for_dupe_key == if we see the same column object multiple - # times under a particular name, whether it's the _label name or the - # anon label, apply _dedupe_label_anon_label to the subsequent - # occurrences of it. - if self._label_style is LABEL_STYLE_NONE: - # don't generate any labels - same_cols = set() + key_naming_convention = SelectState._column_naming_convention( + self._label_style + ) - return [ - (None, c, c in same_cols or same_cols.add(c)) for c in cols - ] - else: - names = {} + names = {} - use_tablename_labels = ( - self._label_style is LABEL_STYLE_TABLENAME_PLUS_COL - ) + result = [] + result_append = result.append - def name_for_col(c): - if not c._render_label_in_columns_clause: - return (None, c, False) - elif use_tablename_labels: - if c._label is None: - repeated = c._anon_name_label in names - names[c._anon_name_label] = c - return (None, c, repeated) - else: - name = effective_name = c._label - elif getattr(c, "name", None) is None: - # this is a scalar_select(). need to improve this case + table_qualified = self._label_style is LABEL_STYLE_TABLENAME_PLUS_COL + label_style_none = self._label_style is LABEL_STYLE_NONE + + for c in cols: + repeated = False + + if not c._render_label_in_columns_clause: + effective_name = ( + required_label_name + ) = fallback_label_name = None + elif label_style_none: + effective_name = required_label_name = None + fallback_label_name = c._non_anon_label or c._anon_name_label + else: + if table_qualified: + required_label_name = ( + effective_name + ) = fallback_label_name = c._tq_label + else: + effective_name = fallback_label_name = c._non_anon_label + required_label_name = None + + if effective_name is None: + # it seems like this could be _proxy_key and we would + # not need _expression_label but it isn't + # giving us a clue when to use anon_label instead expr_label = c._expression_label if expr_label is None: repeated = c._anon_name_label in names names[c._anon_name_label] = c - return (None, c, repeated) - else: - name = effective_name = expr_label - else: - name = None - effective_name = c.name + effective_name = required_label_name = None - repeated = False + if repeated: + # here, "required_label_name" is sent as + # "None" and "fallback_label_name" is sent. + if table_qualified: + fallback_label_name = c._dedupe_anon_tq_label + else: + fallback_label_name = c._dedupe_anon_label + else: + fallback_label_name = c._anon_name_label + else: + required_label_name = ( + effective_name + ) = fallback_label_name = expr_label + if effective_name is not None: if effective_name in names: # when looking to see if names[name] is the same column as # c, use hash(), so that an annotated version of the column @@ -5915,82 +5917,97 @@ class Select( # different column under the same name. apply # disambiguating label - if use_tablename_labels: - name = c._label_anon_label + if table_qualified: + required_label_name = ( + fallback_label_name + ) = c._anon_tq_label else: - name = c._anon_name_label + required_label_name = ( + fallback_label_name + ) = c._anon_name_label - if anon_for_dupe_key and name in names: - # here, c._label_anon_label is definitely unique to + if anon_for_dupe_key and required_label_name in names: + # here, c._anon_tq_label is definitely unique to # that column identity (or annotated version), so # this should always be true. # this is also an infrequent codepath because # you need two levels of duplication to be here - assert hash(names[name]) == hash(c) + assert hash(names[required_label_name]) == hash(c) # the column under the disambiguating label is # already present. apply the "dedupe" label to # subsequent occurrences of the column so that the # original stays non-ambiguous - if use_tablename_labels: - name = c._dedupe_label_anon_label + if table_qualified: + required_label_name = ( + fallback_label_name + ) = c._dedupe_anon_tq_label else: - name = c._dedupe_anon_label + required_label_name = ( + fallback_label_name + ) = c._dedupe_anon_label repeated = True else: - names[name] = c + names[required_label_name] = c elif anon_for_dupe_key: # same column under the same name. apply the "dedupe" # label so that the original stays non-ambiguous - if use_tablename_labels: - name = c._dedupe_label_anon_label + if table_qualified: + required_label_name = ( + fallback_label_name + ) = c._dedupe_anon_tq_label else: - name = c._dedupe_anon_label + required_label_name = ( + fallback_label_name + ) = c._dedupe_anon_label repeated = True else: names[effective_name] = c - return name, c, repeated - return [name_for_col(c) for c in cols] + result_append( + ( + # string label name, if non-None, must be rendered as a + # label, i.e. "AS <name>" + required_label_name, + # proxy_key that is to be part of the result map for this + # col. this is also the key in a fromclause.c or + # select.selected_columns collection + key_naming_convention(c), + # name that can be used to render an "AS <name>" when + # we have to render a label even though + # required_label_name was not given + fallback_label_name, + # the ColumnElement itself + c, + # True if this is a duplicate of a previous column + # in the list of columns + repeated, + ) + ) + + return result def _generate_fromclause_column_proxies(self, subquery): """Generate column proxies to place in the exported ``.c`` collection of a subquery.""" - keys_seen = set() - prox = [] - - pa = None - - tablename_plus_col = ( - self._label_style is LABEL_STYLE_TABLENAME_PLUS_COL - ) - disambiguate_only = self._label_style is LABEL_STYLE_DISAMBIGUATE_ONLY - - for name, c, repeated in self._generate_columns_plus_names(False): - if c._is_text_clause: - continue - elif tablename_plus_col: - key = c._key_label - if key is not None and key in keys_seen: - if pa is None: - pa = prefix_anon_map() - key = c._label_anon_key_label % pa - keys_seen.add(key) - elif disambiguate_only: - key = c._proxy_key - if key is not None and key in keys_seen: - if pa is None: - pa = prefix_anon_map() - key = c._anon_key_label % pa - keys_seen.add(key) - else: - key = c._proxy_key - prox.append( - c._make_proxy( - subquery, key=key, name=name, name_is_truncatable=True - ) + prox = [ + c._make_proxy( + subquery, + key=proxy_key, + name=required_label_name, + name_is_truncatable=True, ) + for ( + required_label_name, + proxy_key, + fallback_label_name, + c, + repeated, + ) in (self._generate_columns_plus_names(False)) + if not c._is_text_clause + ] + subquery._columns._populate_separate_keys(prox) def _needs_parens_for_grouping(self): |
