]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Clear proxy_set cache when creating an annotated column
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 2 Jul 2019 02:33:26 +0000 (22:33 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 2 Jul 2019 13:57:20 +0000 (09:57 -0400)
Fixed an unlikely issue where the "corresponding column" routine for unions
and other :class:`.CompoundSelect` objects could return the wrong column in
some overlapping column situtations, thus potentially impacting some ORM
operations when set operations are in use, if the underlying
:func:`.select` constructs were used previously in other similar kinds of
routines, due to a cached value not being cleared.

Fixes: #4747
Change-Id: I7fb134cac3604f8fe62e220fb24a0945d0a1c56f

doc/build/changelog/unreleased_13/4747.rst [new file with mode: 0644]
lib/sqlalchemy/sql/elements.py
lib/sqlalchemy/sql/schema.py
lib/sqlalchemy/sql/selectable.py
test/sql/test_selectable.py

diff --git a/doc/build/changelog/unreleased_13/4747.rst b/doc/build/changelog/unreleased_13/4747.rst
new file mode 100644 (file)
index 0000000..dc0a628
--- /dev/null
@@ -0,0 +1,10 @@
+.. change::
+    :tags: bug, sql
+    :tickets: 4747
+
+    Fixed an unlikely issue where the "corresponding column" routine for unions
+    and other :class:`.CompoundSelect` objects could return the wrong column in
+    some overlapping column situtations, thus potentially impacting some ORM
+    operations when set operations are in use, if the underlying
+    :func:`.select` constructs were used previously in other similar kinds of
+    routines, due to a cached value not being cleared.
index cc57e58e5d2a96c0c6ce4a2e7d003b7ec9193d6e..aa7b7f68886b770589485064ed4c0e67ee13df27 100644 (file)
@@ -635,6 +635,7 @@ class ColumnElement(
     __visit_name__ = "column_element"
     primary_key = False
     foreign_keys = []
+    _proxies = ()
 
     _label = None
     """The named label that can be used to target
@@ -783,16 +784,13 @@ class ColumnElement(
 
     @util.memoized_property
     def base_columns(self):
-        return util.column_set(
-            c for c in self.proxy_set if not hasattr(c, "_proxies")
-        )
+        return util.column_set(c for c in self.proxy_set if not c._proxies)
 
     @util.memoized_property
     def proxy_set(self):
         s = util.column_set([self])
-        if hasattr(self, "_proxies"):
-            for c in self._proxies:
-                s.update(c.proxy_set)
+        for c in self._proxies:
+            s.update(c.proxy_set)
         return s
 
     def shares_lineage(self, othercolumn):
@@ -4388,6 +4386,8 @@ class AnnotatedColumnElement(Annotated):
     def __init__(self, element, values):
         Annotated.__init__(self, element, values)
         ColumnElement.comparator._reset(self)
+        if self._proxies:
+            ColumnElement.proxy_set._reset(self)
         for attr in ("name", "key", "table"):
             if self.__dict__.get(attr, False) is None:
                 self.__dict__.pop(attr)
index 62ff25a6461be192d483babc32d969f8c75cf1aa..d39bc9832bf858c8a25355e236fe57f75912bb5e 100644 (file)
@@ -1927,7 +1927,7 @@ class ForeignKey(DialectKWArgs, SchemaItem):
 
         parenttable = self.parent.table
 
-        # assertion, can be commented out.
+        # assertion
         # basically Column._make_proxy() sends the actual
         # target Column to the ForeignKey object, so the
         # string resolution here is never called.
index e38de66e0d7b6bdd431da035168ad2ae0db1c505..2c898e8359eb05000a04ccda288522b55951bb20 100644 (file)
@@ -2759,7 +2759,6 @@ class CompoundSelect(GenerativeSelect):
             # to how low in the list of select()s the column occurs, so
             # that the corresponding_column() operation can resolve
             # conflicts
-
             proxy._proxies = [
                 c._annotate({"weight": i + 1}) for (i, c) in enumerate(cols)
             ]
index bfa96d766e8438f08c4ed6464ce866cbe7d15ee7..1a53cd1d67a33f57894075b9033d40acac8827bb 100644 (file)
@@ -433,6 +433,18 @@ class SelectableTest(
         assert u1.corresponding_column(table1.c.colx) is u1.c.col2
         assert u1.corresponding_column(table1.c.col3) is u1.c.col1
 
+    def test_proxy_set_pollution(self):
+        s1 = select([table1.c.col1, table1.c.col2])
+        s2 = select([table1.c.col2, table1.c.col1])
+
+        for c in s1.c:
+            c.proxy_set
+        for c in s2.c:
+            c.proxy_set
+
+        u1 = union(s1, s2)
+        assert u1.corresponding_column(table1.c.col2) is u1.c.col2
+
     def test_singular_union(self):
         u = union(
             select([table1.c.col1, table1.c.col2, table1.c.col3]),
@@ -1936,6 +1948,27 @@ class AnnotationsTest(fixtures.TestBase):
         assert x_p.compare(x_p_a)
         assert not x_p_a.compare(x_a)
 
+    def test_proxy_set_iteration_includes_annotated(self):
+        from sqlalchemy.schema import Column
+
+        c1 = Column("foo", Integer)
+
+        stmt = select([c1]).alias()
+        proxy = stmt.c.foo
+
+        proxy.proxy_set
+
+        # create an annotated of the column
+        p2 = proxy._annotate({"weight": 10})
+
+        # now see if our annotated version is in that column's
+        # proxy_set, as corresponding_column iterates through proxy_set
+        # in this way
+        d = {}
+        for col in p2.proxy_set:
+            d.update(col._annotations)
+        eq_(d, {"weight": 10})
+
     def test_late_name_add(self):
         from sqlalchemy.schema import Column