From: Mike Bayer Date: Tue, 30 Apr 2024 19:41:04 +0000 (-0400) Subject: ensure result_map objects collection is non-empty X-Git-Tag: rel_2_0_30~9^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=f505795a931f3aba1709cb0b731d6fbd74007b38;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git ensure result_map objects collection is non-empty 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 (cherry picked from commit fbb7172c69402d5f0776edc96d1c23a7cfabd3d0) --- diff --git a/doc/build/changelog/unreleased_20/11306.rst b/doc/build/changelog/unreleased_20/11306.rst new file mode 100644 index 0000000000..c5d4ebfb70 --- /dev/null +++ b/doc/build/changelog/unreleased_20/11306.rst @@ -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. + + diff --git a/lib/sqlalchemy/engine/cursor.py b/lib/sqlalchemy/engine/cursor.py index a885aca8e3..b83cb45154 100644 --- a/lib/sqlalchemy/engine/cursor.py +++ b/lib/sqlalchemy/engine/cursor.py @@ -688,6 +688,7 @@ class CursorResultMetaData(ResultMetaData): % (num_ctx_cols, len(cursor_description)) ) seen = set() + for ( idx, colname, diff --git a/lib/sqlalchemy/sql/compiler.py b/lib/sqlalchemy/sql/compiler.py index 9aa06be25a..6d6d8278af 100644 --- a/lib/sqlalchemy/sql/compiler.py +++ b/lib/sqlalchemy/sql/compiler.py @@ -2936,7 +2936,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) @@ -4386,6 +4386,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 @@ -4459,7 +4464,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 diff --git a/test/sql/test_resultset.py b/test/sql/test_resultset.py index 2fd16d46db..93c5c89296 100644 --- a/test/sql/test_resultset.py +++ b/test/sql/test_resultset.py @@ -2573,6 +2573,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