]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
Warn in execute when parameter is an empty list
authorCarlos Sousa <edu-eduardo99@hotmail.com>
Mon, 8 Jan 2024 19:50:02 +0000 (14:50 -0500)
committerFederico Caselli <cfederico87@gmail.com>
Fri, 3 May 2024 18:17:00 +0000 (20:17 +0200)
An empty sequence passed to any ``execute()`` method now
raised a deprecation warning, since such an executemany
is invalid.
Pull request courtesy of Carlos Sousa.

Fixes: #9647
Closes: #10406
Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/10406
Pull-request-sha: 087ba2d88d079b361e30e698e251c022b5780a3d

Change-Id: I482e91a047477c3156a3ca806e5c1eefb6224b95

12 files changed:
doc/build/changelog/unreleased_21/9647.rst [new file with mode: 0644]
lib/sqlalchemy/engine/_util_cy.py
lib/sqlalchemy/orm/session.py
lib/sqlalchemy/testing/suite/test_deprecations.py
lib/sqlalchemy/testing/suite/test_select.py
test/dialect/oracle/test_types.py
test/engine/test_execute.py
test/engine/test_processors.py
test/orm/test_session.py
test/perf/compiled_extensions/misc.py
test/perf/compiled_extensions/row.py
test/requirements.py

diff --git a/doc/build/changelog/unreleased_21/9647.rst b/doc/build/changelog/unreleased_21/9647.rst
new file mode 100644 (file)
index 0000000..f933b08
--- /dev/null
@@ -0,0 +1,8 @@
+.. change::
+    :tags: change, engine
+    :tickets: 9647
+
+    An empty sequence passed to any ``execute()`` method now
+    raised a deprecation warning, since such an executemany
+    is invalid.
+    Pull request courtesy of Carlos Sousa.
index 156fcce9989513cad8dc04434833edb4dc89f06c..1eaf38f07dde7e378b3f4fe08e71a06cbeeab689 100644 (file)
@@ -11,11 +11,11 @@ from collections.abc import Mapping
 import operator
 from typing import Any
 from typing import Optional
-from typing import Sequence
 from typing import Tuple
 from typing import TYPE_CHECKING
 
-from sqlalchemy import exc
+from .. import exc
+from ..util import warn_deprecated
 
 if TYPE_CHECKING:
     from .interfaces import _CoreAnyExecuteParams
@@ -47,7 +47,7 @@ _Empty_Tuple: Tuple[Any, ...] = cython.declare(tuple, ())
 
 @cython.inline
 @cython.cfunc
-def _is_mapping_or_tuple(value: object) -> cython.bint:
+def _is_mapping_or_tuple(value: object, /) -> cython.bint:
     return (
         isinstance(value, dict)
         or isinstance(value, tuple)
@@ -57,22 +57,7 @@ def _is_mapping_or_tuple(value: object) -> cython.bint:
     )
 
 
-@cython.inline
-@cython.cfunc
-@cython.exceptval(0)
-def _validate_execute_many_item(params: Sequence[Any]) -> cython.bint:
-    ret: cython.bint = 1
-    if len(params) > 0:
-        if not _is_mapping_or_tuple(params[0]):
-            ret = 0
-            raise exc.ArgumentError(
-                "List argument must consist only of tuples or dictionaries"
-            )
-    return ret
-
-
-# _is_mapping_or_tuple and _validate_execute_many_item could be
-# inlined if pure python perf is a problem
+# _is_mapping_or_tuple could be inlined if pure python perf is a problem
 def _distill_params_20(
     params: Optional[_CoreAnyExecuteParams],
 ) -> _CoreMultiExecuteParams:
@@ -81,7 +66,17 @@ def _distill_params_20(
     # Assume list is more likely than tuple
     elif isinstance(params, list) or isinstance(params, tuple):
         # collections_abc.MutableSequence # avoid abc.__instancecheck__
-        _validate_execute_many_item(params)
+        if len(params) == 0:
+            warn_deprecated(
+                "Empty parameter sequence passed to execute(). "
+                "This use is deprecated and will raise an exception in a "
+                "future SQLAlchemy release",
+                "2.1",
+            )
+        elif not _is_mapping_or_tuple(params[0]):
+            raise exc.ArgumentError(
+                "List argument must consist only of tuples or dictionaries"
+            )
         return params
     elif isinstance(params, dict) or isinstance(params, Mapping):
         # only do immutabledict or abc.__instancecheck__ for Mapping after
@@ -98,7 +93,10 @@ def _distill_raw_params(
         return _Empty_Tuple
     elif isinstance(params, list):
         # collections_abc.MutableSequence # avoid abc.__instancecheck__
-        _validate_execute_many_item(params)
+        if len(params) > 0 and not _is_mapping_or_tuple(params[0]):
+            raise exc.ArgumentError(
+                "List argument must consist only of tuples or dictionaries"
+            )
         return params
     elif _is_mapping_or_tuple(params):
         return [params]  # type: ignore[return-value]
index 13b906fe2475fd77d15d989a6fef1e189b4b8eda..b77aa72d22b753592f246166589aa466d6d666fb 100644 (file)
@@ -2252,7 +2252,7 @@ class Session(_SessionClassMethods, EventTarget):
             )
         else:
             result = conn.execute(
-                statement, params or {}, execution_options=execution_options
+                statement, params, execution_options=execution_options
             )
 
         if _scalar_result:
index 07970c03ecbb154f3400284a45f92066032d75fa..dc6a71a901a7b70e036df8bddd72f0624bde8220 100644 (file)
@@ -41,7 +41,7 @@ class DeprecatedCompoundSelectTest(fixtures.TablesTest):
             ],
         )
 
-    def _assert_result(self, conn, select, result, params=()):
+    def _assert_result(self, conn, select, result, params=None):
         eq_(conn.execute(select, params).fetchall(), result)
 
     def test_plain_union(self, connection):
index 866bf09cb5d6bf53f8a10e3f54683bb733b62727..8ab6d57bbea8db223abe59b42a7a145fb2cf2402 100644 (file)
@@ -204,7 +204,7 @@ class FetchLimitOffsetTest(fixtures.TablesTest):
         )
 
     def _assert_result(
-        self, connection, select, result, params=(), set_=False
+        self, connection, select, result, params=None, set_=False
     ):
         if set_:
             query_res = connection.execute(select, params).fetchall()
@@ -214,7 +214,7 @@ class FetchLimitOffsetTest(fixtures.TablesTest):
         else:
             eq_(connection.execute(select, params).fetchall(), result)
 
-    def _assert_result_str(self, select, result, params=()):
+    def _assert_result_str(self, select, result, params=None):
         with config.db.connect() as conn:
             eq_(conn.exec_driver_sql(select, params).fetchall(), result)
 
@@ -734,7 +734,7 @@ class SameNamedSchemaTableTest(fixtures.TablesTest):
 class JoinTest(fixtures.TablesTest):
     __backend__ = True
 
-    def _assert_result(self, select, result, params=()):
+    def _assert_result(self, select, result, params=None):
         with config.db.connect() as conn:
             eq_(conn.execute(select, params).fetchall(), result)
 
@@ -856,7 +856,7 @@ class CompoundSelectTest(fixtures.TablesTest):
             ],
         )
 
-    def _assert_result(self, select, result, params=()):
+    def _assert_result(self, select, result, params=None):
         with config.db.connect() as conn:
             eq_(conn.execute(select, params).fetchall(), result)
 
@@ -1121,7 +1121,7 @@ class ExpandingBoundInTest(fixtures.TablesTest):
             ],
         )
 
-    def _assert_result(self, select, result, params=()):
+    def _assert_result(self, select, result, params=None):
         with config.db.connect() as conn:
             eq_(conn.execute(select, params).fetchall(), result)
 
index 3bf78c105a047316f13a65ebe413e01fb8d94d00..b8396df4fa910e597309de58c1773a545f7badcb 100644 (file)
@@ -39,7 +39,6 @@ from sqlalchemy.dialects.oracle import cx_oracle
 from sqlalchemy.dialects.oracle import oracledb
 from sqlalchemy.sql import column
 from sqlalchemy.sql.sqltypes import NullType
-from sqlalchemy.testing import assert_raises_message
 from sqlalchemy.testing import AssertsCompiledSQL
 from sqlalchemy.testing import eq_
 from sqlalchemy.testing import expect_raises_message
@@ -1047,16 +1046,7 @@ class LOBFetchTest(fixtures.TablesTest):
                 )
             eq_(actual, self.data)
 
-        # this comes from cx_Oracle because these are raw
-        # cx_Oracle.Variable objects
-        if testing.requires.oracle5x.enabled:
-            assert_raises_message(
-                testing.db.dialect.dbapi.ProgrammingError,
-                "LOB variable no longer valid after subsequent fetch",
-                go,
-            )
-        else:
-            go()
+        go()
 
     def test_lobs_with_convert_many_rows(self):
         # even with low arraysize, lobs are fine in autoconvert
index 122c08461d159db1bf030186f7f199abeff7ebfc..31a9c4a70a5a1181f74d761521940f8df2498ab4 100644 (file)
@@ -51,6 +51,7 @@ from sqlalchemy.testing import is_
 from sqlalchemy.testing import is_false
 from sqlalchemy.testing import is_not
 from sqlalchemy.testing import is_true
+from sqlalchemy.testing.assertions import expect_deprecated
 from sqlalchemy.testing.assertsql import CompiledSQL
 from sqlalchemy.testing.provision import normalize_sequence
 from sqlalchemy.testing.schema import Column
@@ -637,14 +638,37 @@ class ExecuteTest(fixtures.TablesTest):
             conn.close()
 
     def test_empty_insert(self, connection):
-        """test that execute() interprets [] as a list with no params"""
+        """test that execute() interprets [] as a list with no params and
+        warns since it has nothing to do with such an executemany.
+        """
         users_autoinc = self.tables.users_autoinc
 
-        connection.execute(
-            users_autoinc.insert().values(user_name=bindparam("name", None)),
-            [],
-        )
-        eq_(connection.execute(users_autoinc.select()).fetchall(), [(1, None)])
+        with expect_deprecated(
+            r"Empty parameter sequence passed to execute\(\). "
+            "This use is deprecated and will raise an exception in a "
+            "future SQLAlchemy release"
+        ):
+            connection.execute(
+                users_autoinc.insert().values(
+                    user_name=bindparam("name", None)
+                ),
+                [],
+            )
+
+        eq_(len(connection.execute(users_autoinc.select()).all()), 1)
+
+    @testing.only_on("sqlite")
+    def test_raw_insert_with_empty_list(self, connection):
+        """exec_driver_sql instead does not raise if an empty list is passed.
+        Let the driver do that if it wants to.
+        """
+        conn = connection
+        with expect_raises_message(
+            tsa.exc.ProgrammingError, "Incorrect number of bindings supplied"
+        ):
+            conn.exec_driver_sql(
+                "insert into users (user_id, user_name) values (?, ?)", []
+            )
 
     @testing.only_on("sqlite")
     def test_execute_compiled_favors_compiled_paramstyle(self):
index d49396e99d3e447f4c8e19a1047e747c0578db88..cdb518c969b8e09a6049c440ed74d1d6323fb1ad 100644 (file)
@@ -10,6 +10,7 @@ from sqlalchemy.testing import eq_
 from sqlalchemy.testing import expect_raises_message
 from sqlalchemy.testing import fixtures
 from sqlalchemy.testing import is_none
+from sqlalchemy.testing.assertions import expect_deprecated
 from sqlalchemy.util import immutabledict
 
 
@@ -144,8 +145,13 @@ class _DistillArgsTest(fixtures.TestBase):
         eq_(self.module._distill_params_20(None), ())
 
     def test_distill_20_empty_sequence(self):
-        eq_(self.module._distill_params_20(()), ())
-        eq_(self.module._distill_params_20([]), [])
+        with expect_deprecated(
+            r"Empty parameter sequence passed to execute\(\). "
+            "This use is deprecated and will raise an exception in a "
+            "future SQLAlchemy release"
+        ):
+            eq_(self.module._distill_params_20(()), ())
+            eq_(self.module._distill_params_20([]), [])
 
     def test_distill_20_sequence_sequence(self):
         eq_(self.module._distill_params_20(((1, 2, 3),)), ((1, 2, 3),))
index e08ab19c6e2d07a5c5b12fea894f9e94e858f8db..6e9720774eb9c60e58cfa96824466468c0056db2 100644 (file)
@@ -129,6 +129,24 @@ class ExecutionTest(_fixtures.FixtureTest):
             ):
                 sess.scalar("select id from users where id=:id", {"id": 7})
 
+    @testing.skip_if(
+        "oracle", "missing SELECT keyword [SQL: INSERT INTO tbl () VALUES ()]"
+    )
+    def test_empty_list_execute(self, metadata, connection):
+        t = Table("tbl", metadata, Column("col", sa.Integer))
+        t.create(connection)
+        sess = Session(bind=connection)
+        sess.execute(t.insert(), {"col": 42})
+
+        with assertions.expect_deprecated(
+            r"Empty parameter sequence passed to execute\(\). "
+            "This use is deprecated and will raise an exception in a "
+            "future SQLAlchemy release"
+        ):
+            sess.execute(t.insert(), [])
+
+        eq_(len(sess.execute(sa.select(t.c.col)).all()), 2)
+
 
 class TransScopingTest(_fixtures.FixtureTest):
     run_inserts = None
index 01ff055b2838418240822113ead2f990e6c9860e..d051cca0b78636d2bcc55499bc6ab064b8a87bdc 100644 (file)
@@ -138,11 +138,6 @@ class DistillParam(Case):
     def none_20(self):
         self.impl._distill_params_20(None)
 
-    @test_case
-    def empty_sequence_20(self):
-        self.impl._distill_params_20(())
-        self.impl._distill_params_20([])
-
     @test_case
     def list_20(self):
         self.impl._distill_params_20(self.list_tup)
index 7fe8d0034287d748a99c2fde41dd3af54a348913..227bc8915bc105c4d23bc46baa0f9022ddb7b49a 100644 (file)
@@ -14,12 +14,6 @@ class TupleGetter(Case):
         assert not py_util._is_compiled()
         return py_util.tuplegetter
 
-    @staticmethod
-    def c():
-        from sqlalchemy import cresultproxy
-
-        return cresultproxy.tuplegetter
-
     @staticmethod
     def cython():
         from sqlalchemy.engine import _util_cy
@@ -29,7 +23,6 @@ class TupleGetter(Case):
 
     IMPLEMENTATIONS = {
         "python": python.__func__,
-        "c": c.__func__,
         "cython": cython.__func__,
     }
 
index 2e80884bc17aec686c9d779809c9b1c7692281d9..f8f62fafafdf37a6c932bce7def8d92a2df14312 100644 (file)
@@ -463,7 +463,7 @@ class DefaultRequirements(SuiteRequirements):
 
     @property
     def returning_star(self):
-        """backend supports RETURNING *"""
+        """backend supports ``RETURNING *``"""
 
         return skip_if(["oracle", "mssql"])
 
@@ -1870,13 +1870,6 @@ class DefaultRequirements(SuiteRequirements):
 
         return only_if(go)
 
-    @property
-    def oracle5x(self):
-        return only_if(
-            lambda config: against(config, "oracle+cx_oracle")
-            and config.db.dialect.cx_oracle_ver < (6,)
-        )
-
     @property
     def fail_on_oracledb_thin(self):
         def go(config):