]> git.ipfire.org Git - thirdparty/sqlalchemy/sqlalchemy.git/commitdiff
PostgreSQL / SQLite / Insert.on_conflict_do_update: respect compile_kwargs
authorLoïc Simon <loic.simon@napta.io>
Thu, 5 Feb 2026 19:56:26 +0000 (14:56 -0500)
committerMike Bayer <mike_mp@zzzcomputing.com>
Sun, 8 Feb 2026 20:09:54 +0000 (15:09 -0500)
Fixed issue where :meth:`_postgresql.Insert.on_conflict_do_update`
as well as  :meth:`_sqlite.Insert.on_conflict_do_update`
parameters were not respecting compilation options such as
``literal_binds=True``.

Pull request courtesy Loïc Simon.

Fixes: #13110
Closes: #13111
Pull-request: https://github.com/sqlalchemy/sqlalchemy/pull/13111
Pull-request-sha: 9ca251610b2eb1c5fdda6aeffa6e81dcaef23aaa

Change-Id: Ice21e508210d682098104c78e77bad8d24e6c93f
(cherry picked from commit 6b09777e3d1ef35eb0ed07843b51d1a525702b79)

doc/build/changelog/unreleased_20/13110.rst [new file with mode: 0644]
lib/sqlalchemy/dialects/postgresql/base.py
lib/sqlalchemy/dialects/sqlite/base.py
test/dialect/postgresql/test_compiler.py
test/dialect/sqlite/test_compiler.py

diff --git a/doc/build/changelog/unreleased_20/13110.rst b/doc/build/changelog/unreleased_20/13110.rst
new file mode 100644 (file)
index 0000000..0567071
--- /dev/null
@@ -0,0 +1,16 @@
+.. change::
+    :tags: bug, postgresql
+    :tickets: 13110
+
+    Fixed issue where :meth:`_postgresql.Insert.on_conflict_do_update`
+    parameters were not respecting compilation options such as
+    ``literal_binds=True``.  Pull request courtesy Loïc Simon.
+
+
+.. change::
+    :tags: bug, sqlite
+    :tickets: 13110
+
+    Fixed issue where :meth:`_sqlite.Insert.on_conflict_do_update`
+    parameters were not respecting compilation options such as
+    ``literal_binds=True``.  Pull request courtesy Loïc Simon.
index 4bdb6b50dc3908777cebd7d8be05b5744a66b6dc..7746ca22467c4dc844d90b65ecf9a1bbfc78f630 100644 (file)
@@ -2195,10 +2195,11 @@ class PGCompiler(compiler.SQLCompiler):
                 for c in clause.inferred_target_elements
             )
             if clause.inferred_target_whereclause is not None:
+                whereclause_kw = dict(kw)
+                whereclause_kw.update(include_table=False, use_schema=False)
                 target_text += " WHERE %s" % self.process(
                     clause.inferred_target_whereclause,
-                    include_table=False,
-                    use_schema=False,
+                    **whereclause_kw,
                 )
         else:
             target_text = ""
@@ -2225,6 +2226,8 @@ class PGCompiler(compiler.SQLCompiler):
 
         insert_statement = self.stack[-1]["selectable"]
         cols = insert_statement.table.c
+        set_kw = dict(kw)
+        set_kw.update(use_schema=False)
         for c in cols:
             col_key = c.key
 
@@ -2247,7 +2250,7 @@ class PGCompiler(compiler.SQLCompiler):
                 ):
                     value = value._clone()
                     value.type = c.type
-            value_text = self.process(value.self_group(), use_schema=False)
+            value_text = self.process(value.self_group(), **set_kw)
 
             key_text = self.preparer.quote(c.name)
             action_set_ops.append("%s = %s" % (key_text, value_text))
@@ -2270,14 +2273,16 @@ class PGCompiler(compiler.SQLCompiler):
                 )
                 value_text = self.process(
                     coercions.expect(roles.ExpressionElementRole, v),
-                    use_schema=False,
+                    **set_kw,
                 )
                 action_set_ops.append("%s = %s" % (key_text, value_text))
 
         action_text = ", ".join(action_set_ops)
         if clause.update_whereclause is not None:
+            where_kw = dict(kw)
+            where_kw.update(include_table=True, use_schema=False)
             action_text += " WHERE %s" % self.process(
-                clause.update_whereclause, include_table=True, use_schema=False
+                clause.update_whereclause, **where_kw
             )
 
         return "ON CONFLICT %s DO UPDATE SET %s" % (target_text, action_text)
index 400e91355e5e43b1a2efd9abb12ffb43aeabd8dd..e221f1aed6a3ec12ad27a03b2cc9a10a00a83765 100644 (file)
@@ -1592,12 +1592,16 @@ class SQLiteCompiler(compiler.SQLCompiler):
                 for c in clause.inferred_target_elements
             )
             if clause.inferred_target_whereclause is not None:
-                target_text += " WHERE %s" % self.process(
-                    clause.inferred_target_whereclause,
+                whereclause_kw = dict(kw)
+                whereclause_kw.update(
                     include_table=False,
                     use_schema=False,
                     literal_execute=True,
                 )
+                target_text += " WHERE %s" % self.process(
+                    clause.inferred_target_whereclause,
+                    **whereclause_kw,
+                )
 
         else:
             target_text = ""
@@ -1624,6 +1628,8 @@ class SQLiteCompiler(compiler.SQLCompiler):
 
         insert_statement = self.stack[-1]["selectable"]
         cols = insert_statement.table.c
+        set_kw = dict(kw)
+        set_kw.update(use_schema=False)
         for c in cols:
             col_key = c.key
 
@@ -1644,7 +1650,7 @@ class SQLiteCompiler(compiler.SQLCompiler):
                 ):
                     value = value._clone()
                     value.type = c.type
-            value_text = self.process(value.self_group(), use_schema=False)
+            value_text = self.process(value.self_group(), **set_kw)
 
             key_text = self.preparer.quote(c.name)
             action_set_ops.append("%s = %s" % (key_text, value_text))
@@ -1663,18 +1669,20 @@ class SQLiteCompiler(compiler.SQLCompiler):
                 key_text = (
                     self.preparer.quote(k)
                     if isinstance(k, str)
-                    else self.process(k, use_schema=False)
+                    else self.process(k, **set_kw)
                 )
                 value_text = self.process(
                     coercions.expect(roles.ExpressionElementRole, v),
-                    use_schema=False,
+                    **set_kw,
                 )
                 action_set_ops.append("%s = %s" % (key_text, value_text))
 
         action_text = ", ".join(action_set_ops)
         if clause.update_whereclause is not None:
+            where_kw = dict(kw)
+            where_kw.update(include_table=True, use_schema=False)
             action_text += " WHERE %s" % self.process(
-                clause.update_whereclause, include_table=True, use_schema=False
+                clause.update_whereclause, **where_kw
             )
 
         return "ON CONFLICT %s DO UPDATE SET %s" % (target_text, action_text)
index f73fa8e37fa2034595574cf1472448b4775d6a1b..38eda0cfd2affbfa6d48e704eab819c3a81b9e31 100644 (file)
@@ -1,3 +1,4 @@
+import contextlib
 import random
 
 from sqlalchemy import and_
@@ -3773,6 +3774,67 @@ class InsertOnConflictTest(fixtures.TablesTest, AssertsCompiledSQL):
             },
         )
 
+    @testing.variation(
+        "path", ["unknown_columns", "whereclause", "indexwhere"]
+    )
+    def test_on_conflict_literal_binds(self, path: testing.Variation):
+        """test for #13110"""
+
+        i = insert(self.table_with_metadata).values(myid=1, name="foo")
+
+        if path.unknown_columns:
+            i = i.on_conflict_do_update(
+                index_elements=["myid"],
+                set_=OrderedDict(
+                    [
+                        ("name", "I'm a name"),
+                        ("other_param", literal("this too")),
+                    ]
+                ),
+            )
+            expected = (
+                "ON CONFLICT (myid) DO UPDATE SET name = "
+                "'I''m a name', other_param = 'this too'"
+            )
+            warnings = expect_warnings(
+                "Additional column names not matching any column keys"
+            )
+        elif path.whereclause:
+            i = i.on_conflict_do_update(
+                index_elements=["myid"],
+                set_={"name": "I'm a name"},
+                where=self.table_with_metadata.c.name == "foo",
+            )
+            expected = (
+                "ON CONFLICT (myid) DO UPDATE SET name = "
+                "'I''m a name' WHERE mytable.name = 'foo'"
+            )
+            warnings = contextlib.nullcontext()
+        elif path.indexwhere:
+            i = i.on_conflict_do_update(
+                index_elements=["myid"],
+                set_={"name": "I'm a name"},
+                index_where=self.goofy_index.dialect_options["postgresql"][
+                    "where"
+                ],
+            )
+            warnings = contextlib.nullcontext()
+            expected = (
+                "ON CONFLICT (myid) WHERE name > 'm' "
+                "DO UPDATE SET name = 'I''m a name'"
+            )
+        else:
+            path.fail()
+
+        with warnings:
+            self.assert_compile(
+                i,
+                "INSERT INTO mytable (myid, name) VALUES (1, 'foo')"
+                f" {expected}",
+                {},
+                literal_binds=True,
+            )
+
 
 class DistinctOnTest(fixtures.MappedTest, AssertsCompiledSQL):
     """Test 'DISTINCT' with SQL expression language and orm.Query with
index 169c5688d77129c7cc8b25d043d99e0ba4376fa0..d077fca7629a96a55418ab1915a01af8802521ff 100644 (file)
@@ -1,5 +1,8 @@
 """SQLite-specific tests."""
 
+from collections import OrderedDict
+import contextlib
+
 from sqlalchemy import and_
 from sqlalchemy import CheckConstraint
 from sqlalchemy import Column
@@ -757,6 +760,78 @@ class OnConflictCompileTest(AssertsCompiledSQL, fixtures.TestBase):
             "SET name = excluded.name, login_email = excluded.login_email",
         )
 
+    @testing.variation(
+        "path", ["unknown_columns", "whereclause", "indexwhere"]
+    )
+    def test_on_conflict_literal_binds(self, path: testing.Variation):
+        """test for #13110"""
+
+        metadata = MetaData()
+        table_with_metadata = Table(
+            "mytable",
+            metadata,
+            Column("myid", Integer, primary_key=True),
+            Column("name", String(128)),
+        )
+        goofy_index = Index(
+            "goofy_index",
+            table_with_metadata.c.name,
+            sqlite_where=table_with_metadata.c.name > "m",
+        )
+
+        i = insert(table_with_metadata).values(myid=1, name="foo")
+
+        if path.unknown_columns:
+            i = i.on_conflict_do_update(
+                index_elements=["myid"],
+                set_=OrderedDict(
+                    [
+                        ("name", "I'm a name"),
+                        ("other_param", literal("this too")),
+                    ]
+                ),
+            )
+            expected = (
+                "ON CONFLICT (myid) DO UPDATE SET name = "
+                "'I''m a name', other_param = 'this too'"
+            )
+            warnings = testing.expect_warnings(
+                "Additional column names not matching any column keys"
+            )
+        elif path.whereclause:
+            i = i.on_conflict_do_update(
+                index_elements=["myid"],
+                set_={"name": "I'm a name"},
+                where=table_with_metadata.c.name == "foo",
+            )
+            expected = (
+                "ON CONFLICT (myid) DO UPDATE SET name = "
+                "'I''m a name' WHERE mytable.name = 'foo'"
+            )
+            warnings = contextlib.nullcontext()
+        elif path.indexwhere:
+            i = i.on_conflict_do_update(
+                index_elements=["myid"],
+                set_={"name": "I'm a name"},
+                index_where=goofy_index.dialect_options["sqlite"]["where"],
+            )
+            warnings = contextlib.nullcontext()
+            expected = (
+                "ON CONFLICT (myid) WHERE name > 'm' "
+                "DO UPDATE SET name = 'I''m a name'"
+            )
+        else:
+            path.fail()
+
+        with warnings:
+            self.assert_compile(
+                i,
+                "INSERT INTO mytable (myid, name) VALUES (1, 'foo')"
+                f" {expected}",
+                {},
+                literal_binds=True,
+            )
+
     @testing.fixture
     def users(self):
         metadata = MetaData()