]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
allow executemany values for ON CONFLICT DO NOTHING
authorMike Bayer <mike_mp@zzzcomputing.com>
Thu, 31 Mar 2022 20:53:43 +0000 (16:53 -0400)
committerMike Bayer <mike_mp@zzzcomputing.com>
Thu, 31 Mar 2022 21:01:40 +0000 (17:01 -0400)
Scaled back a fix made for :ticket:`6581` where "executemany values" mode
for psycopg2 were disabled for all "ON CONFLICT" styles of INSERT, to
not apply to the "ON CONFLICT DO NOTHING" clause, which does not include
any parameters and is safe for "executemany values" mode.  "ON CONFLICT
DO UPDATE" is still blocked from "executemany values" as there may
be additional parameters in the DO UPDATE clause that cannot be batched
(which is the original issue fixed by :ticket:`6581`).

Fixes: #7880
Change-Id: Id3e23a0c6699333409a50148fa8923cb8e564bdc

doc/build/changelog/unreleased_14/7880.rst [new file with mode: 0644]
lib/sqlalchemy/dialects/postgresql/base.py
lib/sqlalchemy/dialects/postgresql/psycopg2.py
lib/sqlalchemy/sql/compiler.py
test/dialect/postgresql/test_dialect.py

diff --git a/doc/build/changelog/unreleased_14/7880.rst b/doc/build/changelog/unreleased_14/7880.rst
new file mode 100644 (file)
index 0000000..9abbac1
--- /dev/null
@@ -0,0 +1,11 @@
+.. change::
+    :tags: bug, postgresql
+    :tickets: 7880
+
+    Scaled back a fix made for :ticket:`6581` where "executemany values" mode
+    for psycopg2 were disabled for all "ON CONFLICT" styles of INSERT, to
+    not apply to the "ON CONFLICT DO NOTHING" clause, which does not include
+    any parameters and is safe for "executemany values" mode.  "ON CONFLICT
+    DO UPDATE" is still blocked from "executemany values" as there may
+    be additional parameters in the DO UPDATE clause that cannot be batched
+    (which is the original issue fixed by :ticket:`6581`).
index bec987c4655ffa704edc39d075b145202662aff8..9bfde47688c627c89bf58f85152c196646d9b218 100644 (file)
@@ -1429,6 +1429,7 @@ import re
 from uuid import UUID as _python_UUID
 
 from . import array as _array
+from . import dml
 from . import hstore as _hstore
 from . import json as _json
 from . import ranges as _ranges
@@ -2452,6 +2453,24 @@ class PGCompiler(compiler.SQLCompiler):
 
         return target_text
 
+    @util.memoized_property
+    def _is_safe_for_fast_insert_values_helper(self):
+        # don't allow fast executemany if _post_values_clause is
+        # present and is not an OnConflictDoNothing. what this means
+        # concretely is that the
+        # "fast insert executemany helper" won't be used, in other
+        # words we won't convert "executemany()" of many parameter
+        # sets into a single INSERT with many elements in VALUES.
+        # We can't apply that optimization safely if for example the
+        # statement includes a clause like "ON CONFLICT DO UPDATE"
+
+        return self.insert_single_values_expr is not None and (
+            self.statement._post_values_clause is None
+            or isinstance(
+                self.statement._post_values_clause, dml.OnConflictDoNothing
+            )
+        )
+
     def visit_on_conflict_do_nothing(self, on_conflict, **kw):
 
         target_text = self._on_conflict_target(on_conflict, **kw)
index dddce5a62917ff92fb57508fcb54bed291eeb8b5..391368c5fc68547da967ece920d7fef957f35ab4 100644 (file)
@@ -712,7 +712,7 @@ class PGDialect_psycopg2(_PGDialect_common_psycopg):
             self.executemany_mode & EXECUTEMANY_VALUES
             and context
             and context.isinsert
-            and context.compiled.insert_single_values_expr
+            and context.compiled._is_safe_for_fast_insert_values_helper
         ):
             executemany_values = (
                 "(%s)" % context.compiled.insert_single_values_expr
index a2f731ac91b29876f6929b0ea400b945658d8951..b7f6d11f64d89496855bf8dc36f1754d9238381e 100644 (file)
@@ -4349,14 +4349,7 @@ class SQLCompiler(Compiled):
                 ]
             )
             text += " VALUES (%s)" % insert_single_values_expr
-            if toplevel and insert_stmt._post_values_clause is None:
-                # don't assign insert_single_values_expr if _post_values_clause
-                # is present.  what this means concretely is that the
-                # "fast insert executemany helper" won't be used, in other
-                # words we won't convert "executemany()" of many parameter
-                # sets into a single INSERT with many elements in VALUES.
-                # We can't apply that optimization safely if for example the
-                # statement includes a clause like "ON CONFLICT DO UPDATE"
+            if toplevel:
                 self.insert_single_values_expr = insert_single_values_expr
 
         if insert_stmt._post_values_clause is not None:
index 5a92ae6fe073f0f11fd09e9cfec3bb31ed884d13..fb3a522fc12693337fb55f27a6547f8e2ed00796 100644 (file)
@@ -31,6 +31,7 @@ from sqlalchemy import text
 from sqlalchemy import TypeDecorator
 from sqlalchemy.dialects.postgresql import base as postgresql
 from sqlalchemy.dialects.postgresql import HSTORE
+from sqlalchemy.dialects.postgresql import insert as pg_insert
 from sqlalchemy.dialects.postgresql import JSONB
 from sqlalchemy.dialects.postgresql import psycopg as psycopg_dialect
 from sqlalchemy.dialects.postgresql import psycopg2 as psycopg2_dialect
@@ -322,7 +323,10 @@ class ExecuteManyMode:
             Column("\u6e2c\u8a66", Integer),
         )
 
-    def test_insert(self, connection):
+    @testing.combinations(
+        "insert", "pg_insert", "pg_insert_on_conflict", argnames="insert_type"
+    )
+    def test_insert(self, connection, insert_type):
         from psycopg2 import extras
 
         values_page_size = connection.dialect.executemany_values_page_size
@@ -342,11 +346,23 @@ class ExecuteManyMode:
         else:
             assert False
 
+        if insert_type == "pg_insert_on_conflict":
+            stmt += " ON CONFLICT DO NOTHING"
+
         with mock.patch.object(
             extras, meth.__name__, side_effect=meth
         ) as mock_exec:
+            if insert_type == "insert":
+                ins_stmt = self.tables.data.insert()
+            elif insert_type == "pg_insert":
+                ins_stmt = pg_insert(self.tables.data)
+            elif insert_type == "pg_insert_on_conflict":
+                ins_stmt = pg_insert(self.tables.data).on_conflict_do_nothing()
+            else:
+                assert False
+
             connection.execute(
-                self.tables.data.insert(),
+                ins_stmt,
                 [
                     {"x": "x1", "y": "y1"},
                     {"x": "x2", "y": "y2"},