-
-
Notifications
You must be signed in to change notification settings - Fork 33.1k
Fixed #32519 -- Added support for using key and path transforms in update() for JSONFields. #18489
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
Changes from all commits
d4ba266
3600f05
b90bf8f
5e71358
67dc0f9
3973d4e
bba67d6
f6f8148
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,7 @@ | |
from django.db import NotSupportedError, connections, router | ||
from django.db.models import expressions, lookups | ||
from django.db.models.constants import LOOKUP_SEP | ||
from django.db.models.fields import TextField | ||
from django.db.models.fields import NOT_PROVIDED, TextField | ||
from django.db.models.lookups import ( | ||
FieldGetDbPrepValueMixin, | ||
PostgresOperatorLookup, | ||
|
@@ -340,12 +340,16 @@ def __init__(self, key_name, *args, **kwargs): | |
super().__init__(*args, **kwargs) | ||
self.key_name = str(key_name) | ||
|
||
def preprocess_lhs(self, compiler, connection): | ||
def unwrap_transforms(self): | ||
key_transforms = [self.key_name] | ||
previous = self.lhs | ||
while isinstance(previous, KeyTransform): | ||
key_transforms.insert(0, previous.key_name) | ||
previous = previous.lhs | ||
return previous, key_transforms | ||
|
||
def preprocess_lhs(self, compiler, connection): | ||
previous, key_transforms = self.unwrap_transforms() | ||
adzshaf marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
lhs, params = compiler.compile(previous) | ||
if connection.vendor == "oracle": | ||
# Escape string-formatting. | ||
|
@@ -390,6 +394,19 @@ def as_sqlite(self, compiler, connection): | |
"THEN JSON_TYPE(%s, %%s) ELSE JSON_EXTRACT(%s, %%s) END)" | ||
) % (lhs, datatype_values, lhs, lhs), (tuple(params) + (json_path,)) * 3 | ||
|
||
def get_update_expression(self, value, lhs=None): | ||
from ..functions import JSONRemove, JSONSet | ||
|
||
field, key_transforms = self.unwrap_transforms() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There's a bit of round trip around the transforms, so let me explain the flow.
Alternatively, we could also make it so that the field itself, rather than the transform, defines the expression. This lets us skip the round trip of splitting/joining on For example, if this This approach will result in a much more straightforward code flow. However, it would mean that any use of I think, to make this extensible and more consistent with |
||
|
||
if lhs is None: | ||
lhs = field | ||
|
||
if value is NOT_PROVIDED: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like the idea of using a sentinel to differentiate between There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree @charettes! This was a temporary solution as suggested by Markus the other day, and I linked to your exact comment: https://fosstodon.org/@laymonage/112949402890177435 (Like I said, big fan 😂) Happy to introduce a new sentinel for this, although if we want to also support that sentinel in
That's what I thought too – though I suppose it could still break if people check for the boolean explicitly e.g.
No particular reason, really. When we implemented the first pass for the Sarah and I discussed this at the DjangoCon US sprints and we decided to split this PR into two. One for the DB functions, and one for the nicer syntactic sugar via @adzshaf and I are still working on polishing the DB functions to be ready for a new, smaller PR. We found a few more edge cases so it's taking us a bit longer than we thought. We'll post an update on the ticket when we put the PR up 😄
Thank you, and yes there are definitely more areas of the ORM that we haven't considered like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the update, it's great to see you push all of these JSONField improvement forward. It's has been a game changer to be able to use I think that the two-commit approach has a lot of merit considering how hard it is to get these things right the first time. Let me know when things have shappen if you'd like more feedback. As I said already I think the proposed approach is great in the sense that it demonstrates that it can be done without too many invasive changes. |
||
return JSONRemove(lhs, LOOKUP_SEP.join(key_transforms)) | ||
|
||
return JSONSet(lhs, **{LOOKUP_SEP.join(key_transforms): value}) | ||
|
||
|
||
class KeyTextTransform(KeyTransform): | ||
postgres_operator = "->>" | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -22,6 +22,7 @@ def as_sql(self, compiler, connection, **extra_context): | |||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
def as_sqlite(self, compiler, connection, **extra_context): | ||||||||||||||||||||||||||||||||||||
db_type = self.output_field.db_type(connection) | ||||||||||||||||||||||||||||||||||||
output_type = self.output_field.get_internal_type() | ||||||||||||||||||||||||||||||||||||
if db_type in {"datetime", "time"}: | ||||||||||||||||||||||||||||||||||||
# Use strftime as datetime/time don't keep fractional seconds. | ||||||||||||||||||||||||||||||||||||
template = "strftime(%%s, %(expressions)s)" | ||||||||||||||||||||||||||||||||||||
|
@@ -36,6 +37,11 @@ def as_sqlite(self, compiler, connection, **extra_context): | |||||||||||||||||||||||||||||||||||
return super().as_sql( | ||||||||||||||||||||||||||||||||||||
compiler, connection, template=template, **extra_context | ||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||
elif output_type == "JSONField": | ||||||||||||||||||||||||||||||||||||
template = "JSON(%(expressions)s)" | ||||||||||||||||||||||||||||||||||||
return super().as_sql( | ||||||||||||||||||||||||||||||||||||
compiler, connection, template=template, **extra_context | ||||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||||
|
def datetime_cast_date_sql(self, sql, params, tzname): | |
""" | |
Return the SQL to cast a datetime value to date value. | |
""" | |
raise NotImplementedError( | |
"subclasses of BaseDatabaseOperations may require a " | |
"datetime_cast_date_sql() method." | |
) | |
def datetime_cast_time_sql(self, sql, params, tzname): | |
""" | |
Return the SQL to cast a datetime value to time value. | |
""" | |
raise NotImplementedError( | |
"subclasses of BaseDatabaseOperations may require a " | |
"datetime_cast_time_sql() method" | |
) |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,244 @@ | ||||||||||||||||||||||||||||||||
from django.db import NotSupportedError | ||||||||||||||||||||||||||||||||
from django.db.models.constants import LOOKUP_SEP | ||||||||||||||||||||||||||||||||
from django.db.models.expressions import Func, Value | ||||||||||||||||||||||||||||||||
from django.db.models.fields.json import compile_json_path | ||||||||||||||||||||||||||||||||
from django.db.models.functions import Cast | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
class JSONSet(Func): | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
def __init__(self, expression, output_field=None, **fields): | ||||||||||||||||||||||||||||||||
if not fields: | ||||||||||||||||||||||||||||||||
raise TypeError("JSONSet requires at least one key-value pair to be set.") | ||||||||||||||||||||||||||||||||
self.fields = fields | ||||||||||||||||||||||||||||||||
super().__init__(expression, output_field=output_field) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
def resolve_expression( | ||||||||||||||||||||||||||||||||
self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False | ||||||||||||||||||||||||||||||||
): | ||||||||||||||||||||||||||||||||
c = super().resolve_expression(query, allow_joins, reuse, summarize, for_save) | ||||||||||||||||||||||||||||||||
# Resolve expressions in the JSON update values. | ||||||||||||||||||||||||||||||||
c.fields = { | ||||||||||||||||||||||||||||||||
key: ( | ||||||||||||||||||||||||||||||||
value.resolve_expression(query, allow_joins, reuse, summarize, for_save) | ||||||||||||||||||||||||||||||||
if hasattr(value, "resolve_expression") | ||||||||||||||||||||||||||||||||
else value | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
for key, value in self.fields.items() | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
return c | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
def as_sql( | ||||||||||||||||||||||||||||||||
self, | ||||||||||||||||||||||||||||||||
compiler, | ||||||||||||||||||||||||||||||||
connection, | ||||||||||||||||||||||||||||||||
function=None, | ||||||||||||||||||||||||||||||||
template=None, | ||||||||||||||||||||||||||||||||
arg_joiner=None, | ||||||||||||||||||||||||||||||||
**extra_context, | ||||||||||||||||||||||||||||||||
): | ||||||||||||||||||||||||||||||||
if not connection.features.supports_partial_json_update: | ||||||||||||||||||||||||||||||||
raise NotSupportedError( | ||||||||||||||||||||||||||||||||
"JSONSet() is not supported on this database backend." | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
copy = self.copy() | ||||||||||||||||||||||||||||||||
new_source_expressions = copy.get_source_expressions() | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
for key, value in self.fields.items(): | ||||||||||||||||||||||||||||||||
key_paths = key.split(LOOKUP_SEP) | ||||||||||||||||||||||||||||||||
key_paths_join = compile_json_path(key_paths) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
if not hasattr(value, "resolve_expression"): | ||||||||||||||||||||||||||||||||
# Use Value to serialize the data to string, | ||||||||||||||||||||||||||||||||
# then use Cast to ensure the string is treated as JSON. | ||||||||||||||||||||||||||||||||
value = Cast( | ||||||||||||||||||||||||||||||||
Value(value, output_field=self.output_field), | ||||||||||||||||||||||||||||||||
output_field=self.output_field, | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
Comment on lines
+51
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This is unlike saving values directly to the column, in which case the DBs will happily accept a JSON-formatted string and do any conversion to the actual JSON data type (if they have one). |
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
new_source_expressions.extend((Value(key_paths_join), value)) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
copy.set_source_expressions(new_source_expressions) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
return super(JSONSet, copy).as_sql( | ||||||||||||||||||||||||||||||||
compiler, | ||||||||||||||||||||||||||||||||
connection, | ||||||||||||||||||||||||||||||||
function="JSON_SET", | ||||||||||||||||||||||||||||||||
**extra_context, | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
def as_postgresql(self, compiler, connection, **extra_context): | ||||||||||||||||||||||||||||||||
copy = self.copy() | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
all_items = list(self.fields.items()) | ||||||||||||||||||||||||||||||||
key, value = all_items[0] | ||||||||||||||||||||||||||||||||
rest = all_items[1:] | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
# JSONB_SET does not support arbitrary number of arguments, | ||||||||||||||||||||||||||||||||
# so convert multiple updates into recursive calls. | ||||||||||||||||||||||||||||||||
if rest: | ||||||||||||||||||||||||||||||||
copy.fields = {key: value} | ||||||||||||||||||||||||||||||||
return JSONSet(copy, **dict(rest)).as_postgresql( | ||||||||||||||||||||||||||||||||
compiler, connection, **extra_context | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
new_source_expressions = copy.get_source_expressions() | ||||||||||||||||||||||||||||||||
key_paths = key.split(LOOKUP_SEP) | ||||||||||||||||||||||||||||||||
key_paths_join = ",".join(key_paths) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
new_source_expressions.append(Value(f"{{{key_paths_join}}}")) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
if not hasattr(value, "resolve_expression"): | ||||||||||||||||||||||||||||||||
new_source_expressions.append(Value(value, output_field=self.output_field)) | ||||||||||||||||||||||||||||||||
Comment on lines
+90
to
+91
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't need |
||||||||||||||||||||||||||||||||
else: | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
class ToJSONB(Func): | ||||||||||||||||||||||||||||||||
function = "TO_JSONB" | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
new_source_expressions.append( | ||||||||||||||||||||||||||||||||
ToJSONB(value, output_field=self.output_field), | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
Comment on lines
+94
to
+99
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Casting to JSONB with |
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
copy.set_source_expressions(new_source_expressions) | ||||||||||||||||||||||||||||||||
return super(JSONSet, copy).as_sql( | ||||||||||||||||||||||||||||||||
compiler, connection, function="JSONB_SET", **extra_context | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
def as_oracle(self, compiler, connection, **extra_context): | ||||||||||||||||||||||||||||||||
if not connection.features.supports_partial_json_update: | ||||||||||||||||||||||||||||||||
raise NotSupportedError( | ||||||||||||||||||||||||||||||||
"JSONSet() is not supported on this database backend." | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
copy = self.copy() | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
all_items = list(self.fields.items()) | ||||||||||||||||||||||||||||||||
key, value = all_items[0] | ||||||||||||||||||||||||||||||||
rest = all_items[1:] | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
# JSON_TRANSFORM does not support arbitrary number of arguments, | ||||||||||||||||||||||||||||||||
# so convert multiple updates into recursive calls. | ||||||||||||||||||||||||||||||||
if rest: | ||||||||||||||||||||||||||||||||
copy.fields = {key: value} | ||||||||||||||||||||||||||||||||
return JSONSet(copy, **dict(rest)).as_oracle( | ||||||||||||||||||||||||||||||||
compiler, connection, **extra_context | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
new_source_expressions = copy.get_source_expressions() | ||||||||||||||||||||||||||||||||
key_paths = key.split(LOOKUP_SEP) | ||||||||||||||||||||||||||||||||
key_paths_join = compile_json_path(key_paths) | ||||||||||||||||||||||||||||||||
if not hasattr(value, "resolve_expression"): | ||||||||||||||||||||||||||||||||
new_source_expressions.append(Value(value, output_field=self.output_field)) | ||||||||||||||||||||||||||||||||
else: | ||||||||||||||||||||||||||||||||
new_source_expressions.append(value) | ||||||||||||||||||||||||||||||||
copy.set_source_expressions(new_source_expressions) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
class ArgJoiner: | ||||||||||||||||||||||||||||||||
def join(self, args): | ||||||||||||||||||||||||||||||||
if not hasattr(value, "resolve_expression"): | ||||||||||||||||||||||||||||||||
# Interpolate the JSON path directly to the query string, because | ||||||||||||||||||||||||||||||||
# Oracle does not support passing the JSON path using parameter | ||||||||||||||||||||||||||||||||
# binding. | ||||||||||||||||||||||||||||||||
return f"{args[0]}, SET '{key_paths_join}' = {args[-1]} FORMAT JSON" | ||||||||||||||||||||||||||||||||
return f"{args[0]}, SET '{key_paths_join}' = {args[-1]}" | ||||||||||||||||||||||||||||||||
Comment on lines
+136
to
+141
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interpolating the JSON path directly is nothing new: django/django/db/models/fields/json.py Lines 235 to 237 in 5ed7208
Though I think we should move the comment to be outside the
Suggested change
Using
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
return super(JSONSet, copy).as_sql( | ||||||||||||||||||||||||||||||||
compiler, | ||||||||||||||||||||||||||||||||
connection, | ||||||||||||||||||||||||||||||||
function="JSON_TRANSFORM", | ||||||||||||||||||||||||||||||||
arg_joiner=ArgJoiner(), | ||||||||||||||||||||||||||||||||
**extra_context, | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
class JSONRemove(Func): | ||||||||||||||||||||||||||||||||
def __init__(self, expression, *paths): | ||||||||||||||||||||||||||||||||
if not paths: | ||||||||||||||||||||||||||||||||
raise TypeError("JSONRemove requires at least one path to remove") | ||||||||||||||||||||||||||||||||
self.paths = paths | ||||||||||||||||||||||||||||||||
super().__init__(expression) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
def as_sql( | ||||||||||||||||||||||||||||||||
self, | ||||||||||||||||||||||||||||||||
compiler, | ||||||||||||||||||||||||||||||||
connection, | ||||||||||||||||||||||||||||||||
function=None, | ||||||||||||||||||||||||||||||||
template=None, | ||||||||||||||||||||||||||||||||
arg_joiner=None, | ||||||||||||||||||||||||||||||||
**extra_context, | ||||||||||||||||||||||||||||||||
): | ||||||||||||||||||||||||||||||||
if not connection.features.supports_partial_json_update: | ||||||||||||||||||||||||||||||||
raise NotSupportedError( | ||||||||||||||||||||||||||||||||
"JSONRemove() is not supported on this database backend." | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
copy = self.copy() | ||||||||||||||||||||||||||||||||
new_source_expressions = copy.get_source_expressions() | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
for path in self.paths: | ||||||||||||||||||||||||||||||||
key_paths = path.split(LOOKUP_SEP) | ||||||||||||||||||||||||||||||||
key_paths_join = compile_json_path(key_paths) | ||||||||||||||||||||||||||||||||
new_source_expressions.append(Value(key_paths_join)) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
copy.set_source_expressions(new_source_expressions) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
return super(JSONRemove, copy).as_sql( | ||||||||||||||||||||||||||||||||
compiler, | ||||||||||||||||||||||||||||||||
connection, | ||||||||||||||||||||||||||||||||
function="JSON_REMOVE", | ||||||||||||||||||||||||||||||||
**extra_context, | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
def as_postgresql(self, compiler, connection, **extra_context): | ||||||||||||||||||||||||||||||||
copy = self.copy() | ||||||||||||||||||||||||||||||||
path, *rest = self.paths | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
if rest: | ||||||||||||||||||||||||||||||||
copy.paths = (path,) | ||||||||||||||||||||||||||||||||
return JSONRemove(copy, *rest).as_postgresql( | ||||||||||||||||||||||||||||||||
compiler, connection, **extra_context | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
new_source_expressions = copy.get_source_expressions() | ||||||||||||||||||||||||||||||||
key_paths = path.split(LOOKUP_SEP) | ||||||||||||||||||||||||||||||||
key_paths_join = ",".join(key_paths) | ||||||||||||||||||||||||||||||||
new_source_expressions.append(Value(f"{{{key_paths_join}}}")) | ||||||||||||||||||||||||||||||||
copy.set_source_expressions(new_source_expressions) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
return super(JSONRemove, copy).as_sql( | ||||||||||||||||||||||||||||||||
compiler, | ||||||||||||||||||||||||||||||||
connection, | ||||||||||||||||||||||||||||||||
template="%(expressions)s", | ||||||||||||||||||||||||||||||||
arg_joiner="#- ", | ||||||||||||||||||||||||||||||||
**extra_context, | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
def as_oracle(self, compiler, connection, **extra_context): | ||||||||||||||||||||||||||||||||
if not connection.features.supports_partial_json_update: | ||||||||||||||||||||||||||||||||
raise NotSupportedError( | ||||||||||||||||||||||||||||||||
"JSONRemove() is not supported on this database backend." | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
copy = self.copy() | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
all_items = self.paths | ||||||||||||||||||||||||||||||||
path, *rest = all_items | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
if rest: | ||||||||||||||||||||||||||||||||
copy.paths = (path,) | ||||||||||||||||||||||||||||||||
return JSONRemove(copy, *rest).as_oracle( | ||||||||||||||||||||||||||||||||
compiler, connection, **extra_context | ||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
key_paths = path.split(LOOKUP_SEP) | ||||||||||||||||||||||||||||||||
key_paths_join = compile_json_path(key_paths) | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
class ArgJoiner: | ||||||||||||||||||||||||||||||||
def join(self, args): | ||||||||||||||||||||||||||||||||
return f"{args[0]}, REMOVE '{key_paths_join}'" | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
return super(JSONRemove, copy).as_sql( | ||||||||||||||||||||||||||||||||
compiler, | ||||||||||||||||||||||||||||||||
connection, | ||||||||||||||||||||||||||||||||
function="JSON_TRANSFORM", | ||||||||||||||||||||||||||||||||
arg_joiner=ArgJoiner(), | ||||||||||||||||||||||||||||||||
**extra_context, | ||||||||||||||||||||||||||||||||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems correct according to https://apex.oracle.com/database-features/
However, the function seems to be documented for 19c as well: https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/json_transform.html
According to https://stackoverflow.com/a/75286939 it was backported to 19c in 19.10. Could we update this and see if it works? According to https://code.djangoproject.com/wiki/CI we use v19.3.0.0.0 on the CI though, so that may need to be upgraded, or we can test ourselves.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We agreed on following the https://apex.oracle.com/database-features/ website.