PostgREST / postgrest

REST API for any Postgres database
https://postgrest.org
MIT License
23.37k stars 1.03k forks source link

Accept patch format for JSONB - `set.` filter for `PATCH` #465

Open sscarduzio opened 8 years ago

sscarduzio commented 8 years ago

I'd like to add a field in a jsonb field (a la jsonb_set, ref. http://www.postgresql.org/docs/9.5/static/functions-json.html).

Ideally this would happen similarly to when you update part of the columns of a row with the PATCH verb.

Is this in the roadmap?

niquola commented 8 years ago

:+1:

begriffs commented 8 years ago

I can see how it's annoying to replace a big json object entirely just to change one part of it. We can think of ways to allow such a patch, and in the meantime there are two workarounds

sscarduzio commented 8 years ago

On the same note of array patching proposed in #488

Given a table with a jsonb column, and a tuple with primary key 'x' (x, {"w": true, "a": "z"})

PATCH /table/column?id=x
{
 "value": {"a": "b"}
} 

Would affect the row as follows:

(x, {"w": true, "a", "b"})

begriffs commented 8 years ago

Both #488 and this issue are special cases of the general ability to manipulate JSONB in a patch request.

If we're going to extend the behavior of PATCH it probably makes sense to choose a well-known patch format, such as RFC6902. Here is how it specifies adding an array element: https://tools.ietf.org/html/rfc6902#page-12

I'm closing the other issue as a dup and we can continue conversation here. This seems like a nontrivial amount of work!

sscarduzio commented 8 years ago

wait, in what issue are we following up on this?

begriffs commented 8 years ago

If I'm understanding correctly 488 is a special case of this issue, so let's track JSONB patching here.

begriffs commented 7 years ago

After reviewing this, I'm not sure how common the need is. It is also achievable with a stored procedure (documentation about how to call them is here). So I'm closing this issue.

egeste commented 6 years ago

+1, this is definitely a useful feature. I could extract out the datum into it's own column, or perform this behavior in an RPC, but I would strongly prefer to have a baked-in interface for performing PATCHes against a JSONB column

steve-chavez commented 6 years ago

This seems really useful. Implementing that RFC seems unnecessarily complicated(also slower because of the parsing) considering we already have the pg function that does all the work for us.

A simpler approach would be to define our own content type(could be application/vnd.pgrst.patch+json) and then send the payload to jsonb_set, much like the idea sscarduzio commented above.

For anyone interested in this being implemented, you can sponsor development on our Patreon or you could also contact me directly(email in profile) so we can arrange development on a fee basis.

steve-chavez commented 3 years ago

The upcoming jsonb subscripting syntax could help here. We could do:

PATCH /tbl?columns=jsonb_column[key]

{"jsonb_column": "value"}

brackets([]) need to be url-encoded though.

steve-chavez commented 3 years ago

An advantage of adopting RFC6902 is that it could also serve us to increment a value(common operation):

{ "op": "replace", "path": "/a/b/c", "value": " +=3" }
{ "op": "replace", "path": "/a/b/d", "value": "-=1" }

Taken from https://github.com/json-api/json-api/issues/214#issuecomment-49104564

Also a json path lib already available https://github.com/GallagherCommaJack/Haskell-JSON-Patch


It would also serve to update a column based on another column:

PATCH /employees?id=eq.1
Content-Type: application/json-patch+json

{ "op": "copy", "from": "/first_name", "path": "/middle_name" }
micahjon commented 2 years ago

I realize implementing RFC 6902 would be more complex than just using Postgres' built-in functionality, but it would allow developers to use existing JSON diff & patch solutions instead of having to roll their own, for instance:

As someone writing apps, I'd much rather use a battle-tested patching solution at the front-end and API layers, even if it's marginally more expensive at the database layer. To be fair, I'm out of my depth on the exact technical tradeoffs here and could be missing something.

I think especially for libraries like this one (and companies like Supabase) that are building on PostgreSQL with developer ergonomics front and center, compatibility with the broader ecosystem matters and will spur adoption.

steve-chavez commented 2 years ago

Some observations about implementing JSON patch

Drawbacks

Will propose an alternative that doesn't have the drawbacks and it's more flexible.

steve-chavez commented 2 years ago

Alternative

Introduce a new set filter, it takes a list of identifiers(similar to what the in operator does for values) plus reserved keywords, e.g. ?col1=set.(col1,plus,col2,body->values). The reserved keywords are:

This would cover all the UPDATE use cases we've been asked until now.

Use cases

Replace a column based on another one:

PATCH /tbl?col1=set.(col2)&id=eq.1
(Body is unecessary here)

UPDATE tbl SET col1 = col2 WHERE id = 1

Incrementing a counter by a value:

PATCH /tbl?counter=set.(counter,plus,body->value)&id=eq.1
{"value": 5}

UPDATE tbl SET counter = counter + 5 WHERE id = 1

Append to an array:

PATCH /tbl?col_arr=set.(col_arr,concat,body->value)&id=eq.1
{"value": [1,2,3]}

update tbl set col_arr = col_arr || [1,2,3]

JSON Patch replace(add is more or less the same):

PATCH /tbl?jsonb_col->key=set.(body->value)&id=eq.1
{"value": {"another" 3}}

# whenever an arrow(`->` ) is used, we assume `jsonb_set`
update tbl set jsonb_col = jsonb_set(jsonb_col, '{key}', '{"another" 3}') where id = 1

JSON Patch copy:

PATCH /tbl?jsonb_other->a=set.(jsonb_col->b)&id=eq.1
(Body is optional here)

update tbl set jsonb_other = jsonb_set(jsonb_other, '{a}', jsonb_col->b) where id = 1

JSON Patch move(same as copy above plus nullifying):

PATCH /tbl?jsonb_other->a=set.(jsonb_col->b)&jsonb_col->b=set.(null)&id=eq.1
(Body is unecessary here)

update tbl set
jsonb_other = jsonb_set(jsonb_other, '{a}', jsonb_col->b),
jsonb_col = jsonb_set(jsonb_col, '{b}', null)
where id = 1

JSON Patch remove should be just nullifying. JSON Patch test is unneeded in our case.

Advantages

Backwards compat

Right now it can be assumed that all the json keys in the body are used in set.

PATCH /people?age=lt.13 HTTP/1.1

{ "category": "child", "eye_color": "brown" }

Implicitly is:

PATCH /people?age=lt.13&category=set.(body->category)&eye_color=set.(body->eye_color) HTTP/1.1

{ "category": "child", "eye_color": "brown" }

And they can be combined as:

PATCH /people?age=lt.13&age=set.(age,plus,body->age_increment) HTTP/1.1

{ "category": "child", "age_increment": "2" }

# it would translate to
UPDATE people SET category = 'child', age = age + 2 WHERE age < 13

The parentheses inside the set value are to maintain consistency with in but it should be fine omitting them as well.

wolfgangwalther commented 2 years ago

I suppose a slightly different syntax, which is a tiny bit better to read:

PATCH /people?age=lt.13&age=set.age+plus+body->age_increment HTTP/1.1
{ "category": "child", "age_increment": "2" }

Because in your client-app you can most likely write that as age plus body->age_increment, i.e. use spaces instead of +.

This would also keep parentheses and commas for row constructors, to be able to set custom types:

PATCH /coordinates?point=set.(body->x,body-y) HTTP/1.1
{ "x": 1, "y": 2 }

would translate to

UPDATE coordinates SET point = ROW(1,2)
oalexdoda commented 2 years ago

Just do what MongoDB does: https://www.mongodb.com/docs/manual/reference/operator/update/set/#set-fields-in-embedded-documents - it's been available for quite some time.

steve-chavez commented 1 year ago

Edit: not a good idea since the syntax would be tied to a particular media type. Maybe this could work later for urlencoded or some other media.

Just one final improvement that will turn this into a simpler interface.

Using the suggestions on https://github.com/PostgREST/postgrest/issues/2066, we can put the set grammar in a header, by defining a new media type. This way we can avoid the whole body->col convention.

PATCH /people?age=lt.13

Content-Type: application/vnd.pgrst.patch+json; category.set=$category; age.set=age+plus+$age_increment

{ "category": "child", "age_increment": "2" }
PATCH /coordinates HTTP/1.1

Content-Type: application/vnd.pgrst.patch+json; point.set=($x,$y)

{ "x": 1, "y": 2 }
steve-chavez commented 1 year ago

Just one improvement that could make parsing easier, use $body instead of body:

PATCH /people?age=lt.13&age=set.age+plus+$body->age_increment HTTP/1.1
{ "category": "child", "age_increment": "2" }

It would also make more clear that this key has a special meaning.

mckinlde commented 1 year ago

Hi, I am a novice end-user. I found this while looking for a way to do the append to an array use case, as in https://github.com/PostgREST/postgrest/issues/465#issuecomment-1199858002:

In my case jsonb is not relevant; I have a bigint[] and I'd like to pass something like:

     const { data: userData, error: userError } = await patchPgData(
      rdsUri,
      Token,
      `/user?id=eq.${session.user_id}`,
      { saved_articles: `array_append(saved_articles, ${article.article_id})` }
    );

An RPC was suggested in https://github.com/PostgREST/postgrest/issues/488, and while that would work it seems unnecessarily specific. Could the built-in PostgreSQL Array-append function be called from postgrest? That (seems like) would cover many use cases, not require custom RPCs, and allow postgres errors to propogate through end-user http requests as I can attest I am already used to.

AC4G commented 1 year ago

We already have an select query parameter with which we can directly access an value inside of a json (example: GET http://localhost:3000/items?id=eq.2&select=parameter->damage) so wouldn't it be useful to have something similar for updating a specific value?

What I had in mind

yuvalkarmi commented 12 months ago

Hey folks, chiming in here to ask where we stand with this? It would be extremely helpful to have such patch functionality for JSON objects, and I see the discussion about this dates 7 years :)

popadotstudio commented 10 months ago

What would it take to make this happen considering there's this little thing called jsonb_set `been around now :)