]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
dont enter do_executemany if implicit_returning is False
authorMike Bayer <mike_mp@zzzcomputing.com>
Wed, 11 Oct 2023 14:28:34 +0000 (10:28 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Wed, 11 Oct 2023 22:27:27 +0000 (18:27 -0400)
Fixed regression in recently revised "insertmanyvalues" feature (likely
issue :ticket:`9618`) where the ORM would inadvertently attempt to
interpret a non-RETURNING result as one with RETURNING, in the case where
the ``implicit_returning=False`` parameter were applied to the mapped
:class:`.Table`, indicating that "insertmanyvalues" cannot be used if the
primary key values are not provided.

This includes a refinement to insertmanyvalues where we consider
return_defaults() with supplemental_cols to be the same as an explicit
returning(), since that's the purpose of this.  This saves us some
extra exceptions that could be thrown per-dialect if implicit_returning
were set to False in some cases.

Fixed issue within some dialects where the dialect could incorrectly return
an empty result set for an INSERT statement that does not actually return
rows at all, due to artfacts from pre- or post-fetching the primary key of
the row or rows still being present.  Affected dialects included asyncpg,
all mssql dialects.

Fixes: #10453
Change-Id: Ie2e7e4f4cd9180558f9da315d21895347ec6d4f7

12 files changed:
doc/build/changelog/unreleased_20/10453.rst [new file with mode: 0644]
lib/sqlalchemy/dialects/mssql/base.py
lib/sqlalchemy/dialects/mssql/pyodbc.py
lib/sqlalchemy/dialects/postgresql/asyncpg.py
lib/sqlalchemy/engine/default.py
lib/sqlalchemy/orm/persistence.py
lib/sqlalchemy/sql/crud.py
lib/sqlalchemy/sql/dml.py
lib/sqlalchemy/testing/suite/test_insert.py
test/orm/dml/test_bulk_statements.py
test/orm/test_unitofworkv2.py
test/sql/test_returning.py

diff --git a/doc/build/changelog/unreleased_20/10453.rst b/doc/build/changelog/unreleased_20/10453.rst
new file mode 100644 (file)
index 0000000..93ed84a
--- /dev/null
@@ -0,0 +1,19 @@
+.. change::
+    :tags: bug, orm
+    :tickets: 10453
+
+    Fixed regression in recently revised "insertmanyvalues" feature (likely
+    issue :ticket:`9618`) where the ORM would inadvertently attempt to
+    interpret a non-RETURNING result as one with RETURNING, in the case where
+    the ``implicit_returning=False`` parameter were applied to the mapped
+    :class:`.Table`, indicating that "insertmanyvalues" cannot be used if the
+    primary key values are not provided.
+
+.. change::
+    :tags: bug, engine
+
+    Fixed issue within some dialects where the dialect could incorrectly return
+    an empty result set for an INSERT statement that does not actually return
+    rows at all, due to artfacts from pre- or post-fetching the primary key of
+    the row or rows still being present.  Affected dialects included asyncpg,
+    all mssql dialects.
index 6d46687e43b797bc012673480b9755fb20f0d3a3..f23d05fcf514a5ba18e5a67e2154578ff87c7327 100644 (file)
@@ -1942,6 +1942,7 @@ class MSExecutionContext(default.DefaultExecutionContext):
             row = self.cursor.fetchall()[0]
             self._lastrowid = int(row[0])
 
+            self.cursor_fetch_strategy = _cursor._NO_CURSOR_DML
         elif (
             self.compiled is not None
             and is_sql_compiler(self.compiled)
index 49d203ee6e03f215b5286c937a8e89a1acb3abf9..b711998e29fa1a7684b0d26c2406b770815ec48c 100644 (file)
@@ -365,6 +365,7 @@ from ... import exc
 from ... import types as sqltypes
 from ... import util
 from ...connectors.pyodbc import PyODBCConnector
+from ...engine import cursor as _cursor
 
 
 class _ms_numeric_pyodbc:
@@ -593,6 +594,8 @@ class MSExecutionContext_pyodbc(MSExecutionContext):
                     self.cursor.nextset()
 
             self._lastrowid = int(row[0])
+
+            self.cursor_fetch_strategy = _cursor._NO_CURSOR_DML
         else:
             super().post_exec()
 
index 1554879a5cabfd892efd539a7aece9d3f90fad21..4fa188cb02ebf2e303303b901564d865c3d677c5 100644 (file)
@@ -566,6 +566,7 @@ class AsyncAdapt_asyncpg_cursor:
     async def _executemany(self, operation, seq_of_parameters):
         adapt_connection = self._adapt_connection
 
+        self.description = None
         async with adapt_connection._execute_mutex:
             await adapt_connection._check_type_cache_invalidation(
                 self._invalidate_schema_cache_asof
index e9f1efbe637a890fb3b0187535bb40cc291bfab4..553d8f0bea1fb3d2260e7afc8524e7d86752fb0d 100644 (file)
@@ -71,6 +71,7 @@ if typing.TYPE_CHECKING:
     from types import ModuleType
 
     from .base import Engine
+    from .cursor import ResultFetchStrategy
     from .interfaces import _CoreMultiExecuteParams
     from .interfaces import _CoreSingleExecuteParams
     from .interfaces import _DBAPICursorDescription
@@ -1853,7 +1854,7 @@ class DefaultExecutionContext(ExecutionContext):
     def _setup_dml_or_text_result(self):
         compiled = cast(SQLCompiler, self.compiled)
 
-        strategy = self.cursor_fetch_strategy
+        strategy: ResultFetchStrategy = self.cursor_fetch_strategy
 
         if self.isinsert:
             if (
@@ -1882,9 +1883,15 @@ class DefaultExecutionContext(ExecutionContext):
             strategy = _cursor.BufferedRowCursorFetchStrategy(
                 self.cursor, self.execution_options
             )
-        cursor_description = (
-            strategy.alternate_cursor_description or self.cursor.description
-        )
+
+        if strategy is _cursor._NO_CURSOR_DML:
+            cursor_description = None
+        else:
+            cursor_description = (
+                strategy.alternate_cursor_description
+                or self.cursor.description
+            )
+
         if cursor_description is None:
             strategy = _cursor._NO_CURSOR_DML
         elif self._num_sentinel_cols:
index 31fbdf34386a20336eb76d4f3266e0b12508c45d..6729b479f903c4931e973cb16be82ea4d40ef972 100644 (file)
@@ -1082,7 +1082,7 @@ def _emit_insert_statements(
             records = list(records)
 
             if returning_is_required_anyway or (
-                not hasvalue and len(records) > 1
+                table.implicit_returning and not hasvalue and len(records) > 1
             ):
                 if (
                     deterministic_results_reqd
index 298c50ec0f3856a8504215228ea64558404f4d9b..e51403eceda09a096dacb796cad123ad9fae004a 100644 (file)
@@ -1578,7 +1578,11 @@ def _get_returning_modifiers(compiler, stmt, compile_state, toplevel):
         should_implicit_return_defaults = (
             implicit_returning and stmt._return_defaults
         )
-        explicit_returning = should_implicit_return_defaults or stmt._returning
+        explicit_returning = (
+            should_implicit_return_defaults
+            or stmt._returning
+            or stmt._supplemental_returning
+        )
         use_insertmanyvalues = (
             toplevel
             and compiler.for_executemany
index 921aed3f9e73b92a8d1f8b9fb53d2f9a11301f27..4ca6ed338f40e9ae5e5ce017c94afe84dcf5c9d3 100644 (file)
@@ -567,7 +567,8 @@ class UpdateBase(
 
         3. :meth:`.UpdateBase.return_defaults` can be called against any
            backend. Backends that don't support RETURNING will skip the usage
-           of the feature, rather than raising an exception. The return value
+           of the feature, rather than raising an exception, *unless*
+           ``supplemental_cols`` is passed. The return value
            of :attr:`_engine.CursorResult.returned_defaults` will be ``None``
            for backends that don't support RETURNING or for which the target
            :class:`.Table` sets :paramref:`.Table.implicit_returning` to
index a893e30334733501dcb9f75f4abfbb57012b6cbe..0faf591da4990a246d900aa1910c220b51cd48aa 100644 (file)
@@ -104,6 +104,15 @@ class InsertBehaviorTest(fixtures.TablesTest):
             Column("id", Integer, primary_key=True, autoincrement=False),
             Column("data", String(50)),
         )
+        Table(
+            "no_implicit_returning",
+            metadata,
+            Column(
+                "id", Integer, primary_key=True, test_needs_autoincrement=True
+            ),
+            Column("data", String(50)),
+            implicit_returning=False,
+        )
         Table(
             "includes_defaults",
             metadata,
@@ -119,6 +128,33 @@ class InsertBehaviorTest(fixtures.TablesTest):
             ),
         )
 
+    @testing.variation("style", ["plain", "return_defaults"])
+    @testing.variation("executemany", [True, False])
+    def test_no_results_for_non_returning_insert(
+        self, connection, style, executemany
+    ):
+        """test another INSERT issue found during #10453"""
+
+        table = self.tables.no_implicit_returning
+
+        stmt = table.insert()
+        if style.return_defaults:
+            stmt = stmt.return_defaults()
+
+        if executemany:
+            data = [
+                {"data": "d1"},
+                {"data": "d2"},
+                {"data": "d3"},
+                {"data": "d4"},
+                {"data": "d5"},
+            ]
+        else:
+            data = {"data": "d1"}
+
+        r = connection.execute(stmt, data)
+        assert not r.returns_rows
+
     @requirements.autoincrement_insert
     def test_autoclose_on_insert(self, connection):
         r = connection.execute(
index d9c91a57073f1e63c8dc9118690732012c57c14c..7246a9749f21e29c0195809110e86e6fa468262d 100644 (file)
@@ -48,18 +48,21 @@ class InsertStmtTest(testing.AssertsExecutionResults, fixtures.TestBase):
     @testing.variation(
         "style",
         [
+            ("default", testing.requires.insert_returning),
             "no_executemany",
             ("no_sort_by", testing.requires.insert_returning),
             ("all_enabled", testing.requires.insert_returning),
         ],
     )
     @testing.variation("sort_by_parameter_order", [True, False])
+    @testing.variation("enable_implicit_returning", [True, False])
     def test_no_returning_error(
         self,
         decl_base,
         testing_engine,
         style: testing.Variation,
         sort_by_parameter_order,
+        enable_implicit_returning,
     ):
         class A(ComparableEntity, decl_base):
             __tablename__ = "a"
@@ -67,22 +70,30 @@ class InsertStmtTest(testing.AssertsExecutionResults, fixtures.TestBase):
             data: Mapped[str]
             x: Mapped[Optional[int]] = mapped_column("xcol")
 
+            if not enable_implicit_returning:
+                __table_args__ = {"implicit_returning": False}
+
         engine = testing_engine()
 
-        if style.no_executemany:
+        if style.default:
+            pass
+        elif style.no_executemany:
             engine.dialect.use_insertmanyvalues = False
+            engine.dialect.use_insertmanyvalues_wo_returning = False
             engine.dialect.insert_executemany_returning = False
             engine.dialect.insert_executemany_returning_sort_by_parameter_order = (  # noqa: E501
                 False
             )
         elif style.no_sort_by:
             engine.dialect.use_insertmanyvalues = True
+            engine.dialect.use_insertmanyvalues_wo_returning = True
             engine.dialect.insert_executemany_returning = True
             engine.dialect.insert_executemany_returning_sort_by_parameter_order = (  # noqa: E501
                 False
             )
         elif style.all_enabled:
             engine.dialect.use_insertmanyvalues = True
+            engine.dialect.use_insertmanyvalues_wo_returning = True
             engine.dialect.insert_executemany_returning = True
             engine.dialect.insert_executemany_returning_sort_by_parameter_order = (  # noqa: E501
                 True
@@ -93,8 +104,10 @@ class InsertStmtTest(testing.AssertsExecutionResults, fixtures.TestBase):
         decl_base.metadata.create_all(engine)
         s = Session(engine)
 
-        if style.all_enabled or (
-            style.no_sort_by and not sort_by_parameter_order
+        if (
+            style.all_enabled
+            or (style.no_sort_by and not sort_by_parameter_order)
+            or style.default
         ):
             result = s.scalars(
                 insert(A).returning(
index 5ca34d91747d4d1d9f06ac6966a82087706c6a91..1a5b697b8ef65188ef12b4c8283da55a46f0ccdc 100644 (file)
@@ -3160,7 +3160,9 @@ class EagerDefaultsSettingTest(
         t = Table(
             "test",
             metadata,
-            Column("id", Integer, primary_key=True),
+            Column(
+                "id", Integer, primary_key=True, test_needs_autoincrement=True
+            ),
             Column(
                 "foo",
                 Integer,
@@ -3284,6 +3286,126 @@ class EagerDefaultsSettingTest(
             )
         )
 
+    def test_eager_default_setting_inserts_no_pks(
+        self,
+        setup_mappers,
+        eager_defaults_variations,
+        implicit_returning_variations,
+        connection,
+    ):
+        """test for #10453.
+
+        This is essentially a variation from test_eager_default_setting,
+        as a separate test because there are too many new conditions by
+        introducing this variant.
+
+        """
+        Thing = setup_mappers
+        s = Session(connection)
+
+        t1, t2 = (Thing(bar=6), Thing(bar=6))
+
+        s.add_all([t1, t2])
+
+        expected_eager_defaults = eager_defaults_variations.eager_defaults or (
+            (
+                eager_defaults_variations.auto
+                or eager_defaults_variations.unspecified
+            )
+            and connection.dialect.insert_executemany_returning
+            and bool(implicit_returning_variations)
+        )
+        expect_returning = connection.dialect.insert_returning and bool(
+            implicit_returning_variations
+        )
+
+        with self.sql_execution_asserter(connection) as asserter:
+            s.flush()
+
+        asserter.assert_(
+            Conditional(
+                expect_returning,
+                [
+                    Conditional(
+                        connection.dialect.insert_executemany_returning,
+                        [
+                            Conditional(
+                                expected_eager_defaults,
+                                [
+                                    CompiledSQL(
+                                        "INSERT INTO test (bar) "
+                                        "VALUES (:bar) "
+                                        "RETURNING test.id, test.foo",
+                                        [
+                                            {"bar": 6},
+                                            {"bar": 6},
+                                        ],
+                                    )
+                                ],
+                                [
+                                    CompiledSQL(
+                                        "INSERT INTO test (bar) "
+                                        "VALUES (:bar) "
+                                        "RETURNING test.id",
+                                        [
+                                            {"bar": 6},
+                                            {"bar": 6},
+                                        ],
+                                    )
+                                ],
+                            )
+                        ],
+                        [
+                            CompiledSQL(
+                                "INSERT INTO test (bar) "
+                                "VALUES (:bar) "
+                                "RETURNING test.id, test.foo",
+                                {"bar": 6},
+                            ),
+                            CompiledSQL(
+                                "INSERT INTO test (bar) "
+                                "VALUES (:bar) "
+                                "RETURNING test.id, test.foo",
+                                {"bar": 6},
+                            ),
+                        ],
+                    ),
+                ],
+                [
+                    CompiledSQL(
+                        "INSERT INTO test (bar) VALUES (:bar)",
+                        [
+                            {"bar": 6},
+                        ],
+                        enable_returning=False,
+                    ),
+                    CompiledSQL(
+                        "INSERT INTO test (bar) VALUES (:bar)",
+                        [
+                            {"bar": 6},
+                        ],
+                        enable_returning=False,
+                    ),
+                    Conditional(
+                        expected_eager_defaults and not expect_returning,
+                        [
+                            CompiledSQL(
+                                "SELECT test.foo AS test_foo "
+                                "FROM test WHERE test.id = :pk_1",
+                                [{"pk_1": 1}],
+                            ),
+                            CompiledSQL(
+                                "SELECT test.foo AS test_foo "
+                                "FROM test WHERE test.id = :pk_1",
+                                [{"pk_1": 2}],
+                            ),
+                        ],
+                        [],
+                    ),
+                ],
+            )
+        )
+
     def test_eager_default_setting_updates(
         self,
         setup_mappers,
index 7d40fa76f33f16c555b50a0143726c74bae6cb9b..4d55c435db1729e47282066a6aa0b6429c734376 100644 (file)
@@ -1295,6 +1295,16 @@ class InsertManyReturningTest(fixtures.TablesTest):
             Column("strval", String(50)),
         )
 
+        Table(
+            "no_implicit_returning",
+            metadata,
+            Column(
+                "id", Integer, primary_key=True, test_needs_autoincrement=True
+            ),
+            Column("data", String(30)),
+            implicit_returning=False,
+        )
+
     @testing.combinations(
         (
             lambda table: (table.c.strval + "hi",),
@@ -1416,6 +1426,61 @@ class InsertManyReturningTest(fixtures.TablesTest):
                     ],
                 )
 
+    @testing.variation(
+        "style",
+        ["no_cols", "cols", "cols_plus_supplemental", "normal_returning"],
+    )
+    def test_no_executemany_w_no_implicit_returning(self, connection, style):
+        """test a refinement made during fixes for #10453;
+        return_defaults() with 'supplemental_cols' is considered to be an
+        explicit returning case, bypassing the implicit_returning parameter.
+
+        """
+        t1 = self.tables.no_implicit_returning
+
+        if style.cols_plus_supplemental:
+            result = connection.execute(
+                t1.insert().return_defaults(
+                    t1.c.id, supplemental_cols=[t1.c.data]
+                ),
+                [
+                    {"data": "d1"},
+                    {"data": "d2"},
+                    {"data": "d3"},
+                ],
+            )
+            eq_(result.scalars().all(), ["d1", "d2", "d3"])
+        elif style.normal_returning:
+            result = connection.execute(
+                t1.insert().returning(t1.c.data),
+                [
+                    {"data": "d1"},
+                    {"data": "d2"},
+                    {"data": "d3"},
+                ],
+            )
+            eq_(result.scalars().all(), ["d1", "d2", "d3"])
+        elif style.cols:
+            result = connection.execute(
+                t1.insert().return_defaults(t1.c.id),
+                [
+                    {"data": "d1"},
+                    {"data": "d2"},
+                    {"data": "d3"},
+                ],
+            )
+            assert not result.returns_rows
+        elif style.no_cols:
+            result = connection.execute(
+                t1.insert().return_defaults(t1.c.id),
+                [
+                    {"data": "d1"},
+                    {"data": "d2"},
+                    {"data": "d3"},
+                ],
+            )
+            assert not result.returns_rows
+
     def test_insert_executemany_type_test(self, connection):
         t1 = self.tables.type_cases
         result = connection.execute(