>>> print(stmt)
{printsql}INSERT INTO my_table (id, data) VALUES (?, ?) ON CONFLICT DO NOTHING
+.. _sqlite_on_conflict_multiple:
+
+Specifying Multiple ON CONFLICT Clauses
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+SQLite accepts more than one ``ON CONFLICT`` clause within a single INSERT
+statement. The :meth:`_sqlite.Insert.on_conflict_do_update` and
+:meth:`_sqlite.Insert.on_conflict_do_nothing` methods may therefore be
+invoked repeatedly against the same construct, and may be combined with each
+other; each clause renders in the order in which it was established:
+
+.. sourcecode:: pycon+sql
+
+ >>> stmt = insert(my_table).values(id="some_id", data="inserted value")
+ >>> stmt = stmt.on_conflict_do_update(
+ ... index_elements=["id"], set_=dict(data="updated value")
+ ... ).on_conflict_do_nothing(index_elements=["data"])
+ >>> print(stmt)
+ {printsql}INSERT INTO my_table (id, data) VALUES (?, ?)
+ ON CONFLICT (id) DO UPDATE SET data = ?
+ ON CONFLICT (data) DO NOTHING
+
+SQLite tests the clauses in the order given, and applies at most one of them
+to any particular row, that being the first clause whose conflict target
+matches the constraint that was violated.
+
+Only the last ``ON CONFLICT`` clause of a statement may omit its conflict
+target, in which case it fires for any unique violation not already captured
+by a preceding clause. A :meth:`_sqlite.Insert.on_conflict_do_nothing` call
+that omits
+:paramref:`_sqlite.Insert.on_conflict_do_nothing.index_elements` must
+therefore be the last clause established, else
+:class:`.InvalidRequestError` is raised:
+
+.. sourcecode:: pycon+sql
+
+ >>> stmt = insert(my_table).values(id="some_id", data="inserted value")
+ >>> stmt = stmt.on_conflict_do_update(
+ ... index_elements=["id"], set_=dict(data="updated value")
+ ... ).on_conflict_do_nothing()
+ >>> print(stmt)
+ {printsql}INSERT INTO my_table (id, data) VALUES (?, ?)
+ ON CONFLICT (id) DO UPDATE SET data = ?
+ ON CONFLICT DO NOTHING
+
+.. versionadded:: 2.1 Multiple ``ON CONFLICT`` clauses may be established
+ on a single :class:`_sqlite.Insert` construct.
+
.. _sqlite_type_reflection:
Type Reflection
from typing import Dict
from typing import List
from typing import Optional
+from typing import Sequence
from typing import Union
from .._typing import _OnConflictIndexElementsT
from .._typing import _OnConflictIndexWhereT
from .._typing import _OnConflictSetT
from .._typing import _OnConflictWhereT
+from ... import exc
from ... import util
from ...sql import coercions
from ...sql import roles
from ...sql import schema
from ...sql._typing import _DMLTableArgument
-from ...sql.base import _exclusive_against
from ...sql.base import ColumnCollection
from ...sql.base import ReadOnlyColumnCollection
from ...sql.base import SyntaxExtension
"""
return alias(self.table, name="excluded").columns
- _on_conflict_exclusive = _exclusive_against(
- "_post_values_clause",
- msgs={
- "_post_values_clause": "This Insert construct already has "
- "an ON CONFLICT clause established"
- },
- )
-
- @_on_conflict_exclusive
def on_conflict_do_update(
self,
index_elements: _OnConflictIndexElementsT = None,
r"""
Specifies a DO UPDATE SET action for ON CONFLICT clause.
+ This method may be invoked more than once against the same
+ :class:`_sqlite.Insert` construct, where each ``ON CONFLICT`` clause
+ renders in the order in which it was established.
+
+ .. versionadded:: 2.1 Multiple ``ON CONFLICT`` clauses may be
+ established on a single :class:`_sqlite.Insert` construct.
+
+ .. seealso::
+
+ :ref:`sqlite_on_conflict_multiple`
+
:param index_elements:
A sequence consisting of string column names, :class:`_schema.Column`
objects, or other column expression objects that will be used
OnConflictDoUpdate(index_elements, index_where, set_, where)
)
- @_on_conflict_exclusive
def on_conflict_do_nothing(
self,
index_elements: _OnConflictIndexElementsT = None,
"""
Specifies a DO NOTHING action for ON CONFLICT clause.
+ This method may be invoked more than once against the same
+ :class:`_sqlite.Insert` construct, and may be combined with
+ :meth:`_sqlite.Insert.on_conflict_do_update`, where each
+ ``ON CONFLICT`` clause renders in the order in which it was
+ established. As SQLite allows only the last ``ON CONFLICT`` clause
+ of a statement to omit its conflict target, a call that omits
+ :paramref:`_sqlite.Insert.on_conflict_do_nothing.index_elements`
+ must be the last clause established, else
+ :class:`.InvalidRequestError` is raised.
+
+ .. versionadded:: 2.1 Multiple ``ON CONFLICT`` clauses may be
+ established on a single :class:`_sqlite.Insert` construct.
+
+ .. seealso::
+
+ :ref:`sqlite_on_conflict_multiple`
+
:param index_elements:
A sequence consisting of string column names, :class:`_schema.Column`
objects, or other column expression objects that will be used
self.inferred_target_whereclause
) = None
+ def _append_to_existing(
+ self, existing: Sequence[ClauseElement]
+ ) -> Sequence[ClauseElement]:
+ if existing:
+ last = existing[-1]
+ if (
+ isinstance(last, OnConflictClause)
+ and last.inferred_target_elements is None
+ ):
+ raise exc.InvalidRequestError(
+ "This Insert construct already has an ON CONFLICT "
+ "clause that omits a conflict target; such a clause "
+ "must be the last ON CONFLICT clause in the statement"
+ )
+ return [*existing, self]
+
def apply_to_insert(self, insert_stmt: StandardInsert) -> None:
insert_stmt.apply_syntax_extension_point(
- self.append_replacing_same_type, "post_values"
+ self._append_to_existing, "post_values"
)
from sqlalchemy import bindparam
from sqlalchemy import Column
from sqlalchemy import exc
+from sqlalchemy import MetaData
from sqlalchemy import schema
from sqlalchemy import sql
from sqlalchemy import Table
from sqlalchemy import UniqueConstraint
from sqlalchemy.dialects.sqlite import insert
from sqlalchemy.testing import assert_raises
+from sqlalchemy.testing import AssertsCompiledSQL
from sqlalchemy.testing import eq_
from sqlalchemy.testing import expect_raises
from sqlalchemy.testing import fixtures
with expect_raises(ValueError):
insert(self.tables.users).on_conflict_do_update()
- def test_on_conflict_do_no_call_twice(self):
- users = self.tables.users
-
- for stmt in (
- insert(users).on_conflict_do_nothing(),
- insert(users).on_conflict_do_update(
- index_elements=[users.c.id], set_=dict(name="foo")
- ),
- ):
- for meth in (
- stmt.on_conflict_do_nothing,
- stmt.on_conflict_do_update,
- ):
- with testing.expect_raises_message(
- exc.InvalidRequestError,
- "This Insert construct already has an "
- "ON CONFLICT clause established",
- ):
- meth()
-
def test_on_conflict_do_nothing(self, connection):
users = self.tables.users
).fetchall(),
expected_updated,
)
+
+ def _multiple_clauses_data(self):
+ """rows which conflict against the ``users_xtra`` rows set up by
+ :meth:`._exotic_targets_fixture`.
+
+ the first row conflicts on both ``id`` and ``login_email``, so that
+ which ON CONFLICT clause fires for it depends on the order in which
+ the clauses were established; the second row conflicts on ``id``
+ only.
+
+ """
+ return [
+ dict(
+ id=1,
+ name="name1",
+ login_email="name1@gmail.com",
+ lets_index_this="not",
+ ),
+ dict(
+ id=2,
+ name="name2",
+ login_email="name3@gmail.com",
+ lets_index_this="not",
+ ),
+ ]
+
+ @testing.variation("email_clause_first", [True, False])
+ def test_on_conflict_do_update_multiple_clauses(
+ self, connection, email_clause_first
+ ):
+ """test #13113, where the first clause with a matching conflict
+ target is the one that fires for a given row.
+
+ """
+ users = self.tables.users_xtra
+
+ self._exotic_targets_fixture(connection)
+
+ stmt = insert(users)
+
+ if email_clause_first:
+ stmt = stmt.on_conflict_do_update(
+ index_elements=["login_email"],
+ set_=dict(login_email="nord1@gmail.com"),
+ ).on_conflict_do_update(
+ index_elements=[users.c.id], set_=dict(name="name3")
+ )
+ else:
+ stmt = stmt.on_conflict_do_update(
+ index_elements=[users.c.id], set_=dict(name="name3")
+ ).on_conflict_do_update(
+ index_elements=["login_email"],
+ set_=dict(login_email="nord1@gmail.com"),
+ )
+
+ connection.execute(stmt, self._multiple_clauses_data())
+
+ eq_(
+ connection.execute(users.select()).fetchall(),
+ (
+ [
+ (1, "name1", "nord1@gmail.com", "not"),
+ (2, "name3", "name2@gmail.com", "not"),
+ ]
+ if email_clause_first
+ else [
+ (1, "name3", "name1@gmail.com", "not"),
+ (2, "name3", "name2@gmail.com", "not"),
+ ]
+ ),
+ )
+
+ def test_on_conflict_do_update_and_do_nothing(self, connection):
+ """test #13113, mixing DO UPDATE and DO NOTHING clauses."""
+
+ users = self.tables.users_xtra
+
+ self._exotic_targets_fixture(connection)
+
+ stmt = (
+ insert(users)
+ .on_conflict_do_update(
+ index_elements=["login_email"],
+ set_=dict(login_email="nord1@gmail.com"),
+ )
+ .on_conflict_do_nothing(index_elements=[users.c.id])
+ )
+
+ connection.execute(stmt, self._multiple_clauses_data())
+
+ eq_(
+ connection.execute(users.select()).fetchall(),
+ [
+ (1, "name1", "nord1@gmail.com", "not"),
+ (2, "name2", "name2@gmail.com", "not"),
+ ],
+ )
+
+ def test_on_conflict_untargeted_do_nothing_last(self, connection):
+ """test #13113, a DO NOTHING clause with no conflict target is
+ legal as the last clause of the statement.
+
+ """
+ users = self.tables.users_xtra
+
+ self._exotic_targets_fixture(connection)
+
+ stmt = (
+ insert(users)
+ .on_conflict_do_update(
+ index_elements=["login_email"],
+ set_=dict(login_email="nord1@gmail.com"),
+ )
+ .on_conflict_do_nothing()
+ )
+
+ connection.execute(stmt, self._multiple_clauses_data())
+
+ eq_(
+ connection.execute(users.select()).fetchall(),
+ [
+ (1, "name1", "nord1@gmail.com", "not"),
+ (2, "name2", "name2@gmail.com", "not"),
+ ],
+ )
+
+
+class OnConflictCompileTest(fixtures.TestBase, AssertsCompiledSQL):
+ """test #13113, multiple ON CONFLICT clauses on a single statement."""
+
+ __dialect__ = "sqlite"
+
+ @testing.fixture
+ def users(self):
+ return Table(
+ "users",
+ MetaData(),
+ Column("id", Integer, primary_key=True),
+ Column("name", String(50)),
+ Column("login_email", String(50)),
+ )
+
+ def test_multiple_clauses_render_in_order(self, users):
+ stmt = (
+ insert(users)
+ .on_conflict_do_update(
+ index_elements=[users.c.id], set_=dict(name="name1")
+ )
+ .on_conflict_do_nothing(index_elements=[users.c.login_email])
+ .on_conflict_do_update(
+ index_elements=[users.c.name], set_=dict(name="name2")
+ )
+ )
+
+ self.assert_compile(
+ stmt,
+ "INSERT INTO users (id, name, login_email) VALUES (?, ?, ?) "
+ "ON CONFLICT (id) DO UPDATE SET name = ? "
+ "ON CONFLICT (login_email) DO NOTHING "
+ "ON CONFLICT (name) DO UPDATE SET name = ?",
+ )
+
+ def test_untargeted_do_nothing_renders_last(self, users):
+ stmt = (
+ insert(users)
+ .on_conflict_do_update(
+ index_elements=[users.c.id], set_=dict(name="name1")
+ )
+ .on_conflict_do_nothing()
+ )
+
+ self.assert_compile(
+ stmt,
+ "INSERT INTO users (id, name, login_email) VALUES (?, ?, ?) "
+ "ON CONFLICT (id) DO UPDATE SET name = ? "
+ "ON CONFLICT DO NOTHING",
+ )
+
+ def test_untargeted_clause_must_be_last(self, users):
+ stmt = insert(users).on_conflict_do_nothing()
+
+ for add_clause in (
+ lambda: stmt.on_conflict_do_nothing(),
+ lambda: stmt.on_conflict_do_nothing(index_elements=[users.c.id]),
+ lambda: stmt.on_conflict_do_update(
+ index_elements=[users.c.id], set_=dict(name="name2")
+ ),
+ ):
+ with testing.expect_raises_message(
+ exc.InvalidRequestError,
+ "This Insert construct already has an ON CONFLICT clause "
+ "that omits a conflict target; such a clause must be the "
+ "last ON CONFLICT clause in the statement",
+ ):
+ add_clause()