Opened 4 years ago

Last modified 2 days ago

#32519 assigned New feature

Add support for using key and path transforms in update() for JSONFields.

Reported by: Baptiste Mispelon Owned by: Shafiya Adzhani
Component: Database layer (models, ORM) Version: dev
Severity: Normal Keywords:
Cc: Adam Johnson, Florian Demmer, Sage Abdullah, Clément Escolano, elonzh, şuayip üzülmez, John Speno, Carles Pina Estany, Simon Charette Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: yes
Easy pickings: no UI/UX: no

Description

(I'm not sure if this has been discussed/rejected before. I couldn't find references to it in ticket #12990 so I'm assuming it hasn't).

Background

I was trying to write an update(...) query that would modify a key on an existing JSONField based on the value of another column.
I managed to solve it by writing a few database-specific Func [1] but it was trickier than I'd anticipated and I think Django could make things a bit simpler by providing some generic building blocks.

So I spent a bit of time comparing the respective documentations of the four core database and came up with this list of four functions that I think would be good candidates.
For the most part, they share a common name and API and they're also pretty basic operations from which you can build more complex ones.
(keep in mind I'm more familiar with Postgres than the others so I might have gotten a few things wrong)

1 - JSON_INSERT

Add a path+value to an object. Do nothing if the path already exists.

Backend Syntax Notes
PostgreSQL [2] jsonb_insert(object, path, value)
MariaDB/MySQL [3] JSON_INSERT(obj, path, value) Supports multiple path+value pairs
Oracle [4] JSON_TRANSFORM(obj, SET path=value IGNORE ON EXISTING)
SQLite [5] json_insert(obj, path, value) Supports multiple path+value pairs

2 - JSON_REPLACE

Update a path+value in an object. Do nothing if the path does not exist.

Backend Syntax Notes
PostgreSQL [6] jsonb_set(obj, path, value, false)
MariaDB/MySQL [7] JSON_REPLACE(obj, path, value) Supports multiple path+value pairs
Oracle [4] JSON_TRANSFORM(obj, SET path=value IGNORE ON MISSING)
SQLite [8] json_replace(obj, path, value) Supports multiple path+value pairs

2 - JSON_SET

Update a path+value in an object. Create it if the path does not exist.

Backend Syntax Notes
PostgreSQL [6] jsonb_set(obj, path, value, true)
MariaDB/MySQL [9] JSON_SET(obj, path, value) Supports multiple path+value pairs
Oracle [4] JSON_TRANSFORM(obj, SET path=value)
SQLite [10] json_set(obj, path, value) Supports multiple path+value pairs

4 - JSON_REMOVE

Remove a path from an object. Do nothing if the path does not exist.

Backend Syntax Notes
PostgreSQL [11] obj #- path
MariaDB/MySQL [12] JSON_REMOVE(obj, path) Supports multiple paths
Oracle [4] JSON_TRANSFORM, REMOVE path IGNORE ON MISSING
SQLite [13] json_remove(obj, path) Supports multiple paths

[1] https://gist.github.com/bmispelon/d7f4ecc710a3c2f5c6b07ba5453c4398

[2] https://www.postgresql.org/docs/current/functions-json.html#id-1.5.8.22.5.11.2.2.15.1.1.1
[3] https://dev.mysql.com/doc/refman/8.0/en/json-modification-functions.html#function_json-insert
[4] https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/JSON_TRANSFORM.html
[5] https://www.sqlite.org/json1.html#jins

[6] https://www.postgresql.org/docs/current/functions-json.html#id-1.5.8.22.5.11.2.2.13.1.1.1
[7] https://dev.mysql.com/doc/refman/8.0/en/json-modification-functions.html#function_json-replace
[8] https://www.sqlite.org/json1.html#jrepl

[9] https://dev.mysql.com/doc/refman/8.0/en/json-modification-functions.html#function_json-set
[10] https://www.sqlite.org/json1.html#jset

[11] https://www.postgresql.org/docs/current/functions-json.html#FUNCTIONS-JSONB-OP-TABLE
[12] https://dev.mysql.com/doc/refman/8.0/en/json-modification-functions.html#function_json-remove
[13] https://www.sqlite.org/json1.html#jrm

Change History (19)

comment:1 by Adam Johnson, 4 years ago

Cc: Adam Johnson added

comment:2 by Mariusz Felisiak, 4 years ago

Baptiste, Thanks for the ticket. I don't think we need to support all these functions. I would rather focus on using key transforms in QuerySet.update() to which we can use JSON_SET(), e.g.

Article.objects.update(details__headline='new text')
Article.objects.update(details__authors__0=F('author'))

JSON_REPLACE() and JSON_INSERT() can be emulated with this API by filtering existing/not existing keys (see also similar ticket #30200 for ArrayField.)

What do you think?

in reply to:  2 comment:3 by Baptiste Mispelon, 4 years ago

TL;DR: I agree with reducing the scope of this ticket to only JSON_SET and JSON_REMOVE


I hadn't thought of using key transforms to mimic JSON_SET. From a user perspective it does seem nicer than the JSON path syntax (could be lack of familiarity on my part).

However I think it would still be worthwhile to have a more low-level database function like JSON_SET available.
It would allow some use-cases that can't be expressed as simple key transforms: a dynamic key for example, or one that depends on other fields.

As for insert/replace/set, I personally don't really have a usecase for distinguishing between them.
While researching I saw that all 4 db engines made the distinction so I figured it must have been useful for somebody.
But that's not a very convincing argument, so I wouldn't be opposed to only having JSON_SET landed in core.

comment:4 by Mariusz Felisiak, 4 years ago

Summary: Add INSERT/REPLACE/SET/DELETE databases functions for JSONFieldAdd support for using key and path transforms in update() for JSONFields.
Triage Stage: UnreviewedAccepted

Agreed, we can add support for using key and path transforms in update() for JSONFields and add JSONSet() as an option for complex use cases.

comment:5 by allen-munsch, 3 years ago

Owner: changed from nobody to allen-munsch
Status: newassigned

I have a draft PR currently open with a first attempt here:

It currently has tests for sqlite and postgresql.

I think there is room for improvement in the signature, as their is a kwarg that seems like it would be redundant.

For example the usage of "some_json_field" here:

# sqlite
queryset.update(some_json_field=JSONSet(field="some_json_field", fields={"$.item_in_json_field": Value("asdf")}))
# postgresql
queryset.update(some_json_field=JSONSet(field="some_json_field", fields={"{item_in_json_field}": Value('"asdf"')}))

There's also some backend vendor specific wonkiness on F()s and Values, and all of the cases on oracle for the op type.

I'm hoping to continue work on it this week and next.

My thinking currently is that it would be possible to connect JSONSet into the JSONFields update key and transforms ( although I don't know what the key and transform code looks like currently, and was going to look into that this week)

Depending on direction from the core maintainers I could also attempt to include a JSONRemove util, either in the same PR, or in a separate one.

Last edited 3 years ago by allen-munsch (previous) (diff)

comment:6 by allen-munsch, 3 years ago

Owner: allen-munsch removed
Status: assignednew

comment:7 by Florian Demmer, 3 years ago

Cc: Florian Demmer added

comment:8 by Kamil Turek, 2 years ago

Owner: set to Kamil Turek
Status: newassigned

comment:9 by Sage Abdullah, 2 years ago

Cc: Sage Abdullah added

comment:10 by Clément Escolano, 20 months ago

Cc: Clément Escolano added

comment:11 by elonzh, 14 months ago

Cc: elonzh added

comment:12 by şuayip üzülmez, 10 months ago

Cc: şuayip üzülmez added

comment:13 by John Speno, 6 months ago

Cc: John Speno added

comment:14 by Shafiya Adzhani, 6 months ago

Owner: changed from Kamil Turek to Shafiya Adzhani

I will work on this ticket during GSoC. Hopefully we will land these new features soon!

comment:15 by Shafiya Adzhani, 3 months ago

Has patch: set

comment:16 by Sarah Boyce, 2 months ago

Patch needs improvement: set

comment:17 by Carles Pina Estany, 2 months ago

Cc: Carles Pina Estany added

comment:18 by Shafiya Adzhani, 2 days ago

Patch needs improvement: unset

As discussed in the PR, we are going to split the work for this ticket into two parts:

  • Adding JSONSet and JSONRemove
  • Adding support for using key and path transforms in update()

The latter requires non-trivial amount of refactoring in some hot code paths of the ORM. We think it would be best to get the JSONSet and JSONRemove functions in first and "let them simmer for a release". Once people have got the chance to play around with the functions, we will revisit the transform support later.

Here is the link for new PR: https://github.com/django/django/pull/18758

comment:19 by Simon Charette, 2 days ago

Cc: Simon Charette added
Patch needs improvement: set

Left a few comments on PR.

It's in a relatively good shape but there are possible SQL injection vectors with keys and while the documentation points out that JSONSet can be used instead of directly altering a JSONField value on a model instance it doesn't explain why someone would want to do this.

Last edited 2 days ago by Simon Charette (previous) (diff)
Note: See TracTickets for help on using tickets.
Back to Top