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

docs: clarify usage of upsert with surrogate primary keys #3724

Merged
merged 2 commits into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
26 changes: 21 additions & 5 deletions docs/references/api/tables_views.rst
Original file line number Diff line number Diff line change
Expand Up @@ -623,18 +623,34 @@ You can make an upsert with :code:`POST` and the :code:`Prefer: resolution=merge

.. code-block:: bash

curl "http://localhost:3000/employees" \
curl "http://localhost:3000/products" \
-X POST -H "Content-Type: application/json" \
-H "Prefer: resolution=merge-duplicates" \
-d @- << EOF
[
{ "id": 1, "name": "Old employee 1", "salary": 30000 },
{ "id": 2, "name": "Old employee 2", "salary": 42000 },
{ "id": 3, "name": "New employee 3", "salary": 50000 }
{ "sku": "CL2031", "name": "Existing T-shirt", "price": 35 },
{ "sku": "CL2040", "name": "Existing Hoodie", "price": 60 },
{ "sku": "AC1022", "name": "New Cap", "price": 30 }
]
EOF

By default, upsert operates based on the primary key columns, you must specify all of them. You can also choose to ignore the duplicates with :code:`Prefer: resolution=ignore-duplicates`. This works best when the primary key is natural, but it's also possible to use it if the primary key is surrogate (example: "id serial primary key"). For more details read `this issue <https://github.com/PostgREST/postgrest/issues/1118>`_.
By default, upsert operates based on the primary key columns, so you must specify all of them.
You can also choose to ignore the duplicates with :code:`Prefer: resolution=ignore-duplicates`.
Upsert works best when the primary key is natural (e.g. ``sku``).
However, it can work with surrogate primary keys (e.g. ``id serial primary key``), if you also do a :ref:`bulk_insert_default`:

.. code-block:: bash

curl "http://localhost:3000/employees?colums=id,name,salary" \
-X POST -H "Content-Type: application/json" \
-H "Prefer: resolution=merge-duplicates, missing=default" \
-d @- << EOF
[
{ "id": 1, "name": "Existing employee 1", "salary": 30000 },
{ "id": 2, "name": "Existing employee 2", "salary": 42000 },
{ "name": "New employee 3", "salary": 50000 }
]
EOF

.. important::
After creating a table or changing its primary key, you must refresh PostgREST schema cache for upsert to work properly. To learn how to refresh the cache see :ref:`schema_reloading`.
Expand Down
50 changes: 50 additions & 0 deletions test/spec/Feature/Query/UpsertSpec.hs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,32 @@ spec =
, matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates, return=representation", matchContentTypeJson]
}

it "INSERTs and UPDATEs rows with SERIAL surrogate primary keys using Prefer: missing=default" $
request methodPost "/surr_serial_upsert?columns=id,name&select=name,extra" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates"), ("Prefer", "missing=default")]
[json| [
{ "id": 1, "name": "updated value" },
{ "name": "new value" }
]|] `shouldRespondWith` [json| [
{ "name": "updated value", "extra": "existing value" },
{ "name": "new value", "extra": null }
]|]
{ matchStatus = 201
, matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates, missing=default, return=representation", matchContentTypeJson]
}

it "INSERTs and UPDATEs rows with GENERATED BY DEFAULT surrogate primary keys using Prefer: missing=default" $
request methodPost "/surr_gen_default_upsert?columns=id,name&select=name,extra" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates"), ("Prefer", "missing=default")]
[json| [
{ "id": 1, "name": "updated value" },
{ "name": "new value" }
]|] `shouldRespondWith` [json| [
{ "name": "updated value", "extra": "existing value" },
{ "name": "new value", "extra": null }
]|]
{ matchStatus = 201
, matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates, missing=default, return=representation", matchContentTypeJson]
}

it "succeeds if the table has only PK cols and no other cols" $
request methodPost "/only_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]
[json|[ { "id": 1 }, { "id": 2 }, { "id": 4} ]|]
Expand Down Expand Up @@ -192,6 +218,30 @@ spec =
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates, return=representation"]
}

it "INSERTs and UPDATEs rows with SERIAL surrogate primary keys using Prefer: missing=default" $
request methodPost "/surr_serial_upsert?columns=id,name&select=name,extra" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates"), ("Prefer", "missing=default")]
[json| [
{ "id": 1, "name": "updated value" },
{ "name": "new value" }
]|] `shouldRespondWith` [json| [
{ "name": "new value", "extra": null }
]|]
{ matchStatus = 201
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates, missing=default, return=representation", matchContentTypeJson]
}

it "INSERTs and UPDATEs rows with GENERATED BY DEFAULT surrogate primary keys using Prefer: missing=default" $
request methodPost "/surr_gen_default_upsert?columns=id,name&select=name,extra" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates"), ("Prefer", "missing=default")]
[json| [
{ "id": 1, "name": "updated value" },
{ "name": "new value" }
]|] `shouldRespondWith` [json| [
{ "name": "new value", "extra": null }
]|]
{ matchStatus = 201
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates, missing=default, return=representation", matchContentTypeJson]
}

it "succeeds if the table has only PK cols and no other cols" $
request methodPost "/only_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
[json|[ { "id": 1 }, { "id": 2 }, { "id": 3} ]|]
Expand Down
6 changes: 6 additions & 0 deletions test/spec/fixtures/data.sql
Original file line number Diff line number Diff line change
Expand Up @@ -924,3 +924,9 @@ INSERT INTO process_supervisor VALUES (4, 1);
INSERT INTO process_supervisor VALUES (4, 2);
INSERT INTO process_supervisor VALUES (5, 3);
INSERT INTO process_supervisor VALUES (6, 3);

TRUNCATE TABLE surr_serial_upsert CASCADE;
INSERT INTO surr_serial_upsert(name, extra) VALUES ('value', 'existing value');

TRUNCATE TABLE surr_gen_default_upsert CASCADE;
INSERT INTO surr_gen_default_upsert(name, extra) VALUES ('value', 'existing value');
2 changes: 2 additions & 0 deletions test/spec/fixtures/privileges.sql
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ GRANT USAGE ON SEQUENCE
, items3_id_seq
, callcounter_count
, leak_id_seq
, surr_serial_upsert_id_seq
, surr_gen_default_upsert_id_seq
TO postgrest_test_anonymous;

GRANT USAGE ON SEQUENCE channels_id_seq TO postgrest_test_anonymous;
Expand Down
12 changes: 12 additions & 0 deletions test/spec/fixtures/schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3782,3 +3782,15 @@ create table process_supervisor (
supervisor_id int references supervisors(id),
primary key (process_id, supervisor_id)
);

create table surr_serial_upsert (
id serial primary key,
name text,
extra text
);

create table surr_gen_default_upsert (
id int generated by default as identity primary key,
name text,
extra text
);
Loading