]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
- enhance ClauseAdapter / ColumnAdapter to have new behaviors with labels.
authorMike Bayer <mike_mp@zzzcomputing.com>
Sun, 7 Sep 2014 04:01:34 +0000 (00:01 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 7 Sep 2014 04:01:34 +0000 (00:01 -0400)
The "anonymize label" logic is now generalized to ClauseAdapter, and takes
place when the anonymize_labels flag is sent, taking effect for all
.columns lookups as well as within traverse() calls against the label
directly.
- traverse() will also memoize what it gets in columns, so that
calling upon traverse() / .columns against the same Label will
produce the same anonymized label.  This is so that AliasedClass
produces the same anonymized label when it is accessed per-column
(e.g. SomeAlias.some_column) as well as when it is applied to a Query,
and within column loader strategies (e.g. query(SomeAlias)); the
former uses traverse() while the latter uses .columns
- AliasedClass now calls onto ColumnAdapter
- Query also makes sure to use that same ColumnAdapter from the AliasedClass
in all cases
- update the logic from 0.9 in #1068 to make use of the same
_label_resolve_dict we use for #2992, simplifying how that works
and adding support for new scenarios that were pretty broken
(see #3148, #3188)

lib/sqlalchemy/orm/query.py
lib/sqlalchemy/orm/strategies.py
lib/sqlalchemy/orm/util.py
lib/sqlalchemy/sql/compiler.py
lib/sqlalchemy/sql/elements.py
lib/sqlalchemy/sql/util.py
test/sql/test_generative.py
test/sql/test_text.py

index ba557ef7946d842a0717238c33142fb59eadb143..60948293b940f1567a271d21c1fd709c3e403a8f 100644 (file)
@@ -137,10 +137,7 @@ class Query(object):
                             )
                         aliased_adapter = None
                     elif ext_info.is_aliased_class:
-                        aliased_adapter = sql_util.ColumnAdapter(
-                            ext_info.selectable,
-                            ext_info.mapper._equivalent_columns
-                        )
+                        aliased_adapter = ext_info._adapter
                     else:
                         aliased_adapter = None
 
index 84dd6b0456993892be3b38a5e879b6e90f6e308b..cdb501c14f6ec1f8944d6a7413411d89586575fe 100644 (file)
@@ -1242,7 +1242,8 @@ class JoinedLoader(AbstractRelationshipLoader):
         clauses = orm_util.ORMAdapter(
             to_adapt,
             equivalents=self.mapper._equivalent_columns,
-            adapt_required=True, allow_label_resolve=False)
+            adapt_required=True, allow_label_resolve=False,
+            anonymize_labels=True)
         assert clauses.aliased_class is not None
 
         if self.parent_property.direction != interfaces.MANYTOONE:
index 3072d6ffb6bab5aa6abad74771b669a2062e9463..ed2011d4e2195ae68946aa206e179f4b116fa1a7 100644 (file)
@@ -278,7 +278,8 @@ class ORMAdapter(sql_util.ColumnAdapter):
     """
 
     def __init__(self, entity, equivalents=None, adapt_required=False,
-                 chain_to=None, allow_label_resolve=True):
+                 chain_to=None, allow_label_resolve=True,
+                 anonymize_labels=False):
         info = inspection.inspect(entity)
 
         self.mapper = info.mapper
@@ -291,7 +292,8 @@ class ORMAdapter(sql_util.ColumnAdapter):
         sql_util.ColumnAdapter.__init__(
             self, selectable, equivalents, chain_to,
             adapt_required=adapt_required,
-            allow_label_resolve=allow_label_resolve)
+            allow_label_resolve=allow_label_resolve,
+            anonymize_labels=anonymize_labels)
 
     def replace(self, elem):
         entity = elem._annotations.get('parentmapper', None)
@@ -355,6 +357,7 @@ class AliasedClass(object):
         if alias is None:
             alias = mapper._with_polymorphic_selectable.alias(
                 name=name, flat=flat)
+
         self._aliased_insp = AliasedInsp(
             self,
             mapper,
@@ -461,9 +464,9 @@ class AliasedInsp(InspectionAttr):
         self._base_alias = _base_alias or self
         self._use_mapper_path = _use_mapper_path
 
-        self._adapter = sql_util.ClauseAdapter(
+        self._adapter = sql_util.ColumnAdapter(
             selectable, equivalents=mapper._equivalent_columns,
-            adapt_on_names=adapt_on_names)
+            adapt_on_names=adapt_on_names, anonymize_labels=True)
 
         self._adapt_on_names = adapt_on_names
         self._target = mapper.class_
index 4349c97f423239e28080531b998fc9757e9f4480..72dd11eaf5077e98d2914549cb168aec40c7fa1d 100644 (file)
@@ -701,13 +701,7 @@ class SQLCompiler(Compiled):
         # here; we can only add a label in the ORDER BY for an individual
         # label expression in the columns clause.
 
-        # TODO: we should see if we can bring _resolve_label
-        # into this
-
-
-        raw_col = set(l._order_by_label_element.name
-                      for l in order_by_select._raw_columns
-                      if l._order_by_label_element is not None)
+        raw_col = set(order_by_select._label_resolve_dict.keys())
 
         return ", ".join(
             s for s in
@@ -716,7 +710,7 @@ class SQLCompiler(Compiled):
                     self,
                     render_label_as_label=c._order_by_label_element if
                     c._order_by_label_element is not None and
-                    c._order_by_label_element.name in raw_col
+                    c._order_by_label_element._label in raw_col
                     else None,
                     **kw)
                 for c in clauselist.clauses)
index c8504f21f2c8e60fa9ba963f6812c4dabad06e1f..ece6bce9e965bf608f66a975deca3897a85d8626 100644 (file)
@@ -688,6 +688,10 @@ class ColumnElement(operators.ColumnOperators, ClauseElement):
 
     """
 
+    _allow_label_resolve = True
+    """A flag that can be flipped to prevent a column from being resolvable
+    by string label name."""
+
     _alt_names = ()
 
     def self_group(self, against=None):
@@ -704,8 +708,6 @@ class ColumnElement(operators.ColumnOperators, ClauseElement):
         else:
             return super(ColumnElement, self)._negate()
 
-    _allow_label_resolve = True
-
     @util.memoized_property
     def type(self):
         return type_api.NULLTYPE
@@ -1248,6 +1250,8 @@ class TextClause(Executable, ClauseElement):
     # interpreted in a column expression situation
     key = _label = _resolve_label = None
 
+    _allow_label_resolve = False
+
     def __init__(
             self,
             text,
@@ -2943,8 +2947,14 @@ class Label(ColumnElement):
     def get_children(self, **kwargs):
         return self.element,
 
-    def _copy_internals(self, clone=_clone, **kw):
+    def _copy_internals(self, clone=_clone, anonymize_labels=False, **kw):
         self.element = clone(self.element, **kw)
+        if anonymize_labels:
+            self.name = _anonymous_label(
+                '%%(%d %s)s' % (
+                    id(self), getattr(self.element, 'name', 'anon'))
+            )
+            self.key = self._label = self._key_label = self.name
 
     @property
     def _from_objects(self):
index 47ab61fdd6bbbe522342651f2c9402532a88c522..f630f9e935a3eb271e2672e90c32cf46fcadef96 100644 (file)
@@ -488,8 +488,10 @@ class ClauseAdapter(visitors.ReplacingCloningVisitor):
     def __init__(self, selectable, equivalents=None,
                  include=None, exclude=None,
                  include_fn=None, exclude_fn=None,
-                 adapt_on_names=False):
-        self.__traverse_options__ = {'stop_on': [selectable]}
+                 adapt_on_names=False, anonymize_labels=False):
+        self.__traverse_options__ = {
+            'stop_on': [selectable],
+            'anonymize_labels': anonymize_labels}
         self.selectable = selectable
         if include:
             assert not include_fn
@@ -549,9 +551,14 @@ class ColumnAdapter(ClauseAdapter):
     def __init__(self, selectable, equivalents=None,
                  chain_to=None, include=None,
                  exclude=None, adapt_required=False,
-                 allow_label_resolve=True):
+                 adapt_on_names=False,
+                 allow_label_resolve=True,
+                 anonymize_labels=False):
         ClauseAdapter.__init__(self, selectable, equivalents,
-                               include, exclude)
+                               include, exclude,
+                               adapt_on_names=adapt_on_names,
+                               anonymize_labels=anonymize_labels)
+
         if chain_to:
             self.chain(chain_to)
         self.columns = util.populate_column_dict(self._locate_col)
@@ -567,7 +574,13 @@ class ColumnAdapter(ClauseAdapter):
         ac.columns = util.populate_column_dict(ac._locate_col)
         return ac
 
-    adapt_clause = ClauseAdapter.traverse
+    def traverse(self, obj):
+        new_obj = ClauseAdapter.traverse(self, obj)
+        if new_obj is not obj:
+            self.columns[obj] = new_obj
+        return new_obj
+
+    adapt_clause = traverse
     adapt_list = ClauseAdapter.copy_and_process
 
     def _wrap(self, local, wrapped):
@@ -581,11 +594,6 @@ class ColumnAdapter(ClauseAdapter):
         if c is None:
             c = self.adapt_clause(col)
 
-            # anonymize labels in case they have a hardcoded name
-            # see test_eager_relations.py -> SubqueryTest.test_label_anonymizing
-            if isinstance(c, Label):
-                c = c.label(None)
-
         # adapt_required used by eager loading to indicate that
         # we don't trust a result row column that is not translated.
         # this is to prevent a column from being interpreted as that
index 2e3c4b1e8ba010416234665aa057a52d8be1cf4f..1140a1180854331a657472cebf101782daca94ca 100644 (file)
@@ -1207,6 +1207,52 @@ class ClauseAdapterTest(fixtures.TestBase, AssertsCompiledSQL):
             "WHERE c.bid = anon_1.b_aid"
         )
 
+        t1 = table("table1",
+                   column("col1"),
+                   column("col2"),
+                   column("col3"),
+                   )
+        t2 = table("table2",
+                   column("col1"),
+                   column("col2"),
+                   column("col3"),
+                   )
+
+    def test_label_anonymize_one(self):
+        t1a = t1.alias()
+        adapter = sql_util.ClauseAdapter(t1a, anonymize_labels=True)
+
+        expr = select([t1.c.col2]).where(t1.c.col3 == 5).label('expr')
+        expr_adapted = adapter.traverse(expr)
+
+        stmt = select([expr, expr_adapted]).order_by(expr, expr_adapted)
+        self.assert_compile(
+            stmt,
+            "SELECT "
+            "(SELECT table1.col2 FROM table1 WHERE table1.col3 = :col3_1) "
+            "AS expr, "
+            "(SELECT table1_1.col2 FROM table1 AS table1_1 "
+            "WHERE table1_1.col3 = :col3_2) AS anon_1 "
+            "ORDER BY expr, anon_1"
+        )
+
+    def test_label_anonymize_two(self):
+        t1a = t1.alias()
+        adapter = sql_util.ClauseAdapter(t1a, anonymize_labels=True)
+
+        expr = select([t1.c.col2]).where(t1.c.col3 == 5).label(None)
+        expr_adapted = adapter.traverse(expr)
+
+        stmt = select([expr, expr_adapted]).order_by(expr, expr_adapted)
+        self.assert_compile(
+            stmt,
+            "SELECT "
+            "(SELECT table1.col2 FROM table1 WHERE table1.col3 = :col3_1) "
+            "AS anon_1, "
+            "(SELECT table1_1.col2 FROM table1 AS table1_1 "
+            "WHERE table1_1.col3 = :col3_2) AS anon_2 "
+            "ORDER BY anon_1, anon_2"
+        )
 
 class SpliceJoinsTest(fixtures.TestBase, AssertsCompiledSQL):
     __dialect__ = 'default'
index 94627ae073f5b97bf545cc77be6f778785077db6..60d90196e0cfa1c7f9d04fd0f7ffdcc7de30219c 100644 (file)
@@ -1,7 +1,7 @@
 """Test the TextClause and related constructs."""
 
 from sqlalchemy.testing import fixtures, AssertsCompiledSQL, eq_, \
-    assert_raises_message, expect_warnings
+    assert_raises_message, expect_warnings, assert_warnings
 from sqlalchemy import text, select, Integer, String, Float, \
     bindparam, and_, func, literal_column, exc, MetaData, Table, Column,\
     asc, func, desc, union
@@ -680,7 +680,40 @@ class OrderByLabelResolutionTest(fixtures.TestBase, AssertsCompiledSQL):
             "somelabel DESC"
         )
 
-    def test_anonymized_via_columnadapter(self):
+    def test_columnadapter_anonymized(self):
+        """test issue #3148
+
+        Testing the anonymization applied from the ColumnAdapter.columns
+        collection, typically as used in eager loading.
+
+        """
+        exprs = [
+            table1.c.myid,
+            table1.c.name.label('t1name'),
+            func.foo("hoho").label('x')]
+
+        ta = table1.alias()
+        adapter = sql_util.ColumnAdapter(ta, anonymize_labels=True)
+
+        s1 = select([adapter.columns[expr] for expr in exprs]).\
+            apply_labels().order_by("myid", "t1name", "x")
+
+        def go():
+            # the labels here are anonymized, so label naming
+            # can't catch these.
+            self.assert_compile(
+                s1,
+                "SELECT mytable_1.myid AS mytable_1_myid, "
+                "mytable_1.name AS name_1, foo(:foo_2) AS foo_1 "
+                "FROM mytable AS mytable_1 ORDER BY mytable_1.myid, t1name, x"
+            )
+
+        assert_warnings(
+            go,
+            ["Can't resolve label reference 't1name'",
+             "Can't resolve label reference 'x'"], regex=True)
+
+    def test_columnadapter_non_anonymized(self):
         """test issue #3148
 
         Testing the anonymization applied from the ColumnAdapter.columns
@@ -698,10 +731,11 @@ class OrderByLabelResolutionTest(fixtures.TestBase, AssertsCompiledSQL):
         s1 = select([adapter.columns[expr] for expr in exprs]).\
             apply_labels().order_by("myid", "t1name", "x")
 
-        # our "t1name" and "x" labels get modified
+        # labels are maintained
         self.assert_compile(
             s1,
             "SELECT mytable_1.myid AS mytable_1_myid, "
-            "mytable_1.name AS name_1, foo(:foo_2) AS foo_1 "
-            "FROM mytable AS mytable_1 ORDER BY mytable_1.myid, name_1, foo_1"
+            "mytable_1.name AS t1name, foo(:foo_1) AS x "
+            "FROM mytable AS mytable_1 ORDER BY mytable_1.myid, t1name, x"
         )
+