Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

On conflict clause / upserts #816

Merged
merged 32 commits into from
May 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6533ee4
initial commit
dantownsend Apr 27, 2023
0ee5c89
fix tests
dantownsend Apr 28, 2023
c58dbbc
version pin litestar
dantownsend Apr 28, 2023
ccc1538
use typing extensions for Literal
dantownsend Apr 28, 2023
03287bc
make suggested change
dantownsend Apr 28, 2023
87afd15
undo
dantownsend Apr 28, 2023
745630a
Update piccolo/query/mixins.py
dantownsend Apr 28, 2023
d32b89b
Update piccolo/query/mixins.py
dantownsend Apr 28, 2023
85ee5a0
add one extra comma
dantownsend Apr 28, 2023
5623bc8
Merge branch '252-on-conflict-clause' of github.com:telerytech/piccol…
dantownsend Apr 28, 2023
e171a1a
first attempt at docs
dantownsend Apr 28, 2023
96a2257
add `NotImplementedError` for unsupported methods
dantownsend Apr 29, 2023
84cd6ec
fix typo in sqlite version number
dantownsend Apr 29, 2023
627528d
fix tests
dantownsend Apr 29, 2023
aa3d0a0
get tests running for sqlite
dantownsend Apr 30, 2023
8c42dcc
add test for `do nothing`
dantownsend Apr 30, 2023
850481d
add test for violating non target constraint
dantownsend Apr 30, 2023
a29d9f7
remove old comment
dantownsend Apr 30, 2023
6363990
allow multiple on conflict clauses
dantownsend Apr 30, 2023
a9c3e6e
`target` -> `targets`
dantownsend Apr 30, 2023
ee32a1b
add docstring for `test_do_nothing`
dantownsend Apr 30, 2023
ae973ec
add tests for multiple ON CONFLICT clauses
dantownsend Apr 30, 2023
78415d4
add docs for multiple ``on_conflict`` clauses
dantownsend Apr 30, 2023
455200b
add docs for using `all_columns`
dantownsend Apr 30, 2023
d20afff
fix typo in test name
dantownsend Apr 30, 2023
11de6f6
add test for using `all_columns`
dantownsend Apr 30, 2023
ba0e8e2
add test for using an enum to specify the action
dantownsend Apr 30, 2023
0d2ce6d
add a test to make sure `DO UPDATE` with no values raises an exception
dantownsend Apr 30, 2023
bbe4c6f
rename `targets` back to `target`
dantownsend May 2, 2023
2f3881a
integrate @sinisaos tests
dantownsend May 2, 2023
54c02da
move `on_conflict` to its own page
dantownsend May 3, 2023
8c4a99c
refactor the `where` clause
dantownsend May 3, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/src/piccolo/query_clauses/as_of.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
as_of
=====

.. note:: Cockroach only.

You can use ``as_of`` clause with the following queries:

* :ref:`Select`
Expand All @@ -21,5 +23,3 @@ This generates an ``AS OF SYSTEM TIME`` clause. See `documentation <https://www.
This clause accepts a wide variety of time and interval `string formats <https://www.cockroachlabs.com/docs/stable/as-of-system-time.html#using-different-timestamp-formats>`_.

This is very useful for performance, as it will reduce transaction contention across a cluster.

Currently only supported on Cockroach Engine.
1 change: 1 addition & 0 deletions docs/src/piccolo/query_clauses/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ by modifying the return values.
./freeze
./group_by
./offset
./on_conflict
./output
./returning

Expand Down
229 changes: 229 additions & 0 deletions docs/src/piccolo/query_clauses/on_conflict.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
.. _on_conflict:

on_conflict
===========

.. hint:: This is an advanced topic, and first time learners of Piccolo
can skip if they want.

You can use the ``on_conflict`` clause with the following queries:

* :ref:`Insert`

Introduction
------------

When inserting rows into a table, if a unique constraint fails on one or more
of the rows, then the insertion fails.

Using the ``on_conflict`` clause, we can instead tell the database to ignore
the error (using ``DO NOTHING``), or to update the row (using ``DO UPDATE``).

This is sometimes called an **upsert** (update if it already exists else insert).

Example data
------------

If we have the following table:

.. code-block:: python

class Band(Table):
name = Varchar(unique=True)
popularity = Integer()

With this data:

.. csv-table::
:file: ./on_conflict/bands.csv

Let's try inserting another row with the same ``name``, and we'll get an error:

.. code-block:: python

>>> await Band.insert(
... Band(name="Pythonistas", popularity=1200)
... )
Unique constraint error!

``DO NOTHING``
--------------

To ignore the error:

.. code-block:: python

>>> await Band.insert(
... Band(name="Pythonistas", popularity=1200)
... ).on_conflict(
... action="DO NOTHING"
... )

If we fetch the data from the database, we'll see that it hasn't changed:

.. code-block:: python

>>> await Band.select().where(Band.name == "Pythonistas").first()
{'id': 1, 'name': 'Pythonistas', 'popularity': 1000}


``DO UPDATE``
-------------

Instead, if we want to update the ``popularity``:

.. code-block:: python

>>> await Band.insert(
... Band(name="Pythonistas", popularity=1200)
... ).on_conflict(
... action="DO UPDATE",
... values=[Band.popularity]
... )

If we fetch the data from the database, we'll see that it was updated:

.. code-block:: python

>>> await Band.select().where(Band.name == "Pythonistas").first()
{'id': 1, 'name': 'Pythonistas', 'popularity': 1200}

``target``
----------

Using the ``target`` argument, we can specify which constraint we're concerned
with. By specifying ``target=Band.name`` we're only concerned with the unique
constraint for the ``band`` column. If you omit the ``target`` argument, then
it works for all constraints on the table.

.. code-block:: python
:emphasize-lines: 5

>>> await Band.insert(
... Band(name="Pythonistas", popularity=1200)
... ).on_conflict(
... action="DO NOTHING",
... target=Band.name
... )

If you want to target a composite unique constraint, you can do so by passing
in a tuple of columns:

.. code-block:: python
:emphasize-lines: 5

>>> await Band.insert(
... Band(name="Pythonistas", popularity=1200)
... ).on_conflict(
... action="DO NOTHING",
... target=(Band.name, Band.popularity)
... )

You can also specify the name of a constraint using a string:

.. code-block:: python
:emphasize-lines: 5

>>> await Band.insert(
... Band(name="Pythonistas", popularity=1200)
... ).on_conflict(
... action="DO NOTHING",
... target='some_constraint'
... )

``values``
----------

This lets us specify which values to update when a conflict occurs.

By specifying a :class:`Column <piccolo.columns.base.Column>`, this means that
the new value for that column will be used:

.. code-block:: python
:emphasize-lines: 6

# The new popularity will be 1200.
>>> await Band.insert(
... Band(name="Pythonistas", popularity=1200)
... ).on_conflict(
... action="DO UPDATE",
... values=[Band.popularity]
... )

Instead, we can specify a custom value using a tuple:

.. code-block:: python
:emphasize-lines: 6

# The new popularity will be 1111.
>>> await Band.insert(
... Band(name="Pythonistas", popularity=1200)
... ).on_conflict(
... action="DO UPDATE",
... values=[(Band.popularity, 1111)]
... )

If we want to update all of the values, we can use :meth:`all_columns<piccolo.table.Table.all_columns>`.

.. code-block:: python
:emphasize-lines: 5

>>> await Band.insert(
... Band(id=1, name="Pythonistas", popularity=1200)
... ).on_conflict(
... action="DO UPDATE",
... values=Band.all_columns()
... )

``where``
---------

This can be used with ``DO UPDATE``. It gives us more control over whether the
update should be made:

.. code-block:: python
:emphasize-lines: 6

>>> await Band.insert(
... Band(id=1, name="Pythonistas", popularity=1200)
... ).on_conflict(
... action="DO UPDATE",
... values=[Band.popularity],
... where=Band.popularity < 1000
... )

Multiple ``on_conflict`` clauses
--------------------------------

SQLite allows you to specify multiple ``ON CONFLICT`` clauses, but Postgres and
Cockroach don't.

.. code-block:: python

>>> await Band.insert(
... Band(name="Pythonistas", popularity=1200)
... ).on_conflict(
... action="DO UPDATE",
... ...
... ).on_conflict(
... action="DO NOTHING",
... ...
... )

Learn more
----------

* `Postgres docs <https://www.postgresql.org/docs/current/sql-insert.html#SQL-ON-CONFLICT>`_
* `Cockroach docs <https://www.cockroachlabs.com/docs/v2.0/insert.html#on-conflict-clause>`_
* `SQLite docs <https://www.sqlite.org/lang_UPSERT.html>`_

Source
------

.. currentmodule:: piccolo.query.methods.insert

.. automethod:: Insert.on_conflict

.. autoclass:: OnConflictAction
:members:
:undoc-members:
2 changes: 2 additions & 0 deletions docs/src/piccolo/query_clauses/on_conflict/bands.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
id,name,popularity
1,Pythonistas,1000
46 changes: 30 additions & 16 deletions docs/src/piccolo/query_types/insert.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,47 @@
Insert
======

This is used to insert rows into the table.

.. code-block:: python

>>> await Band.insert(Band(name="Pythonistas"))
[{'id': 3}]

We can insert multiple rows in one go:
This is used to bulk insert rows into the table:

.. code-block:: python

await Band.insert(
Band(name="Pythonistas")
Band(name="Darts"),
Band(name="Gophers")
)

-------------------------------------------------------------------------------

add
---
``add``
-------

You can also compose it as follows:
If we later decide to insert additional rows, we can use the ``add`` method:

.. code-block:: python

await Band.insert().add(
Band(name="Darts")
).add(
Band(name="Gophers")
)
query = Band.insert(Band(name="Pythonistas"))

if other_bands:
query = query.add(
Band(name="Darts"),
Band(name="Gophers")
)

await query

-------------------------------------------------------------------------------

Query clauses
-------------

on_conflict
~~~~~~~~~~~

See :ref:`On_Conflict`.


returning
~~~~~~~~~

See :ref:`Returning`.
2 changes: 1 addition & 1 deletion piccolo/apps/asgi/commands/new.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
SERVERS = ["uvicorn", "Hypercorn"]
ROUTERS = ["starlette", "fastapi", "blacksheep", "litestar"]
ROUTER_DEPENDENCIES = {
"litestar": ["litestar>=2.0.0a3"],
"litestar": ["litestar==2.0.0a3"],
}


Expand Down
Loading