]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
ensure result_map objects collection is non-empty
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 30 Apr 2024 19:41:04 +0000 (15:41 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 30 Apr 2024 19:49:36 +0000 (15:49 -0400)
Fixed issue in cursor handling which affected handling of duplicate
:class:`_sql.Column` or similar objcts in the columns clause of
:func:`_sql.select`, both in combination with arbitary :func:`_sql.text()`
clauses in the SELECT list, as well as when attempting to retrieve
:meth:`_engine.Result.mappings` for the object, which would lead to an
internal error.

Fixes: #11306
Change-Id: I418073b2fdba86b2121b6d00eaa40b1805b69bb8

doc/build/changelog/unreleased_20/11306.rst [new file with mode: 0644]
lib/sqlalchemy/engine/cursor.py
lib/sqlalchemy/sql/compiler.py
test/sql/test_resultset.py

diff --git a/doc/build/changelog/unreleased_20/11306.rst b/doc/build/changelog/unreleased_20/11306.rst
new file mode 100644 (file)
index 0000000..c5d4ebf
--- /dev/null
@@ -0,0 +1,12 @@
+.. change::
+    :tags: bug, engine
+    :tickets: 11306
+
+    Fixed issue in cursor handling which affected handling of duplicate
+    :class:`_sql.Column` or similar objcts in the columns clause of
+    :func:`_sql.select`, both in combination with arbitary :func:`_sql.text()`
+    clauses in the SELECT list, as well as when attempting to retrieve
+    :meth:`_engine.Result.mappings` for the object, which would lead to an
+    internal error.
+
+
index 5d141feaa88843e17fca1f2f4cfd7e6229efdc43..3a58e71a935e607e8861aa0d6f70853248d513b8 100644 (file)
@@ -690,6 +690,7 @@ class CursorResultMetaData(ResultMetaData):
                 % (num_ctx_cols, len(cursor_description))
             )
         seen = set()
+
         for (
             idx,
             colname,
index 785d2e935020bd9b6a5802bd3f49434efaa8cbb7..96f8af237e92fbfe85acc8dd3bd1cf74c3c2ee17 100644 (file)
@@ -2938,7 +2938,7 @@ class SQLCompiler(Compiled):
         **kwargs: Any,
     ) -> str:
         if add_to_result_map is not None:
-            add_to_result_map(func.name, func.name, (), func.type)
+            add_to_result_map(func.name, func.name, (func.name,), func.type)
 
         disp = getattr(self, "visit_%s_func" % func.name.lower(), None)
 
@@ -4388,6 +4388,11 @@ class SQLCompiler(Compiled):
         objects: Tuple[Any, ...],
         type_: TypeEngine[Any],
     ) -> None:
+
+        # note objects must be non-empty for cursor.py to handle the
+        # collection properly
+        assert objects
+
         if keyname is None or keyname == "*":
             self._ordered_columns = False
             self._ad_hoc_textual = True
@@ -4461,7 +4466,7 @@ class SQLCompiler(Compiled):
                 _add_to_result_map = add_to_result_map
 
                 def add_to_result_map(keyname, name, objects, type_):
-                    _add_to_result_map(keyname, name, (), type_)
+                    _add_to_result_map(keyname, name, (keyname,), type_)
 
             # if we redefined col_expr for type expressions, wrap the
             # callable with one that adds the original column to the targets
index e6d02da7e9429fa46ca3d53cab010ea5a4172604..26de957e1ef9e7b0043521c6911e5d8d191d2dcb 100644 (file)
@@ -2572,6 +2572,60 @@ class KeyTargetingTest(fixtures.TablesTest):
         eq_(row[6], "d3")
         eq_(row[7], "d3")
 
+    @testing.requires.duplicate_names_in_cursor_description
+    @testing.combinations((None,), (0,), (1,), (2,), argnames="pos")
+    @testing.variation("texttype", ["literal", "text"])
+    def test_dupe_col_targeting(self, connection, pos, texttype):
+        """test #11306"""
+
+        keyed2 = self.tables.keyed2
+        col = keyed2.c.b
+        data_value = "b2"
+
+        cols = [col, col, col]
+        expected = [data_value, data_value, data_value]
+
+        if pos is not None:
+            if texttype.literal:
+                cols[pos] = literal_column("10")
+            elif texttype.text:
+                cols[pos] = text("10")
+            else:
+                texttype.fail()
+
+            expected[pos] = 10
+
+        stmt = select(*cols)
+
+        result = connection.execute(stmt)
+
+        if texttype.text and pos is not None:
+            # when using text(), the name of the col is taken from
+            # cursor.description directly since we don't know what's
+            # inside a text()
+            key_for_text_col = result.cursor.description[pos][0]
+        elif texttype.literal and pos is not None:
+            # for literal_column(), we use the text
+            key_for_text_col = "10"
+
+        eq_(result.all(), [tuple(expected)])
+
+        result = connection.execute(stmt).mappings()
+        if pos is None:
+            eq_(set(result.keys()), {"b", "b__1", "b__2"})
+            eq_(
+                result.all(),
+                [{"b": data_value, "b__1": data_value, "b__2": data_value}],
+            )
+
+        else:
+            eq_(set(result.keys()), {"b", "b__1", key_for_text_col})
+
+            eq_(
+                result.all(),
+                [{"b": data_value, "b__1": data_value, key_for_text_col: 10}],
+            )
+
     def test_columnclause_schema_column_one(self, connection):
         # originally addressed by [ticket:2932], however liberalized
         # Column-targeting rules are deprecated