From: Loïc Simon Date: Thu, 5 Feb 2026 19:56:26 +0000 (-0500) Subject: PostgreSQL / SQLite / Insert.on_conflict_do_update: respect compile_kwargs X-Git-Tag: rel_2_0_47~7 X-Git-Url: http://git.ipfire.org/gitweb/?a=commitdiff_plain;h=03b4136f35cf16a442d8e876dd493ab0c6438c6a;p=thirdparty%2Fsqlalchemy%2Fsqlalchemy.git PostgreSQL / SQLite / Insert.on_conflict_do_update: respect compile_kwargs 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) --- diff --git a/doc/build/changelog/unreleased_20/13110.rst b/doc/build/changelog/unreleased_20/13110.rst new file mode 100644 index 0000000000..05670719bd --- /dev/null +++ b/doc/build/changelog/unreleased_20/13110.rst @@ -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. diff --git a/lib/sqlalchemy/dialects/postgresql/base.py b/lib/sqlalchemy/dialects/postgresql/base.py index 4bdb6b50dc..7746ca2246 100644 --- a/lib/sqlalchemy/dialects/postgresql/base.py +++ b/lib/sqlalchemy/dialects/postgresql/base.py @@ -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) diff --git a/lib/sqlalchemy/dialects/sqlite/base.py b/lib/sqlalchemy/dialects/sqlite/base.py index 400e91355e..e221f1aed6 100644 --- a/lib/sqlalchemy/dialects/sqlite/base.py +++ b/lib/sqlalchemy/dialects/sqlite/base.py @@ -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) diff --git a/test/dialect/postgresql/test_compiler.py b/test/dialect/postgresql/test_compiler.py index f73fa8e37f..38eda0cfd2 100644 --- a/test/dialect/postgresql/test_compiler.py +++ b/test/dialect/postgresql/test_compiler.py @@ -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 diff --git a/test/dialect/sqlite/test_compiler.py b/test/dialect/sqlite/test_compiler.py index 169c5688d7..d077fca762 100644 --- a/test/dialect/sqlite/test_compiler.py +++ b/test/dialect/sqlite/test_compiler.py @@ -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()