From d7d711c68cc070d9b6962f43f97ece097162adcc Mon Sep 17 00:00:00 2001 From: Sage Abdullah Date: Sun, 15 Dec 2024 16:01:45 +0000 Subject: [PATCH] Refs #35718, Refs #32179 -- Moved JSONObject to django.db.models.functions.json. --- django/db/models/functions/__init__.py | 6 +- django/db/models/functions/comparison.py | 62 ------------------ django/db/models/functions/json.py | 64 +++++++++++++++++++ docs/ref/models/database-functions.txt | 55 ++++++++-------- tests/db_functions/json/__init__.py | 0 .../{comparison => json}/test_json_object.py | 0 6 files changed, 98 insertions(+), 89 deletions(-) create mode 100644 django/db/models/functions/json.py create mode 100644 tests/db_functions/json/__init__.py rename tests/db_functions/{comparison => json}/test_json_object.py (100%) diff --git a/django/db/models/functions/__init__.py b/django/db/models/functions/__init__.py index cd7c801894..7e5e31e0f8 100644 --- a/django/db/models/functions/__init__.py +++ b/django/db/models/functions/__init__.py @@ -1,4 +1,4 @@ -from .comparison import Cast, Coalesce, Collate, Greatest, JSONObject, Least, NullIf +from .comparison import Cast, Coalesce, Collate, Greatest, Least, NullIf from .datetime import ( Extract, ExtractDay, @@ -25,6 +25,7 @@ from .datetime import ( TruncWeek, TruncYear, ) +from .json import JSONObject from .math import ( Abs, ACos, @@ -97,7 +98,6 @@ __all__ = [ "Coalesce", "Collate", "Greatest", - "JSONObject", "Least", "NullIf", # datetime @@ -125,6 +125,8 @@ __all__ = [ "TruncTime", "TruncWeek", "TruncYear", + # json + "JSONObject", # math "Abs", "ACos", diff --git a/django/db/models/functions/comparison.py b/django/db/models/functions/comparison.py index 76ef5d219b..11af0c0750 100644 --- a/django/db/models/functions/comparison.py +++ b/django/db/models/functions/comparison.py @@ -1,9 +1,6 @@ """Database functions that do comparisons or type conversions.""" -from django.db import NotSupportedError from django.db.models.expressions import Func, Value -from django.db.models.fields import TextField -from django.db.models.fields.json import JSONField from django.utils.regex_helper import _lazy_re_compile @@ -143,65 +140,6 @@ class Greatest(Func): return super().as_sqlite(compiler, connection, function="MAX", **extra_context) -class JSONObject(Func): - function = "JSON_OBJECT" - output_field = JSONField() - - def __init__(self, **fields): - expressions = [] - for key, value in fields.items(): - expressions.extend((Value(key), value)) - super().__init__(*expressions) - - def as_sql(self, compiler, connection, **extra_context): - if not connection.features.has_json_object_function: - raise NotSupportedError( - "JSONObject() is not supported on this database backend." - ) - return super().as_sql(compiler, connection, **extra_context) - - def join(self, args): - pairs = zip(args[::2], args[1::2], strict=True) - # Wrap 'key' in parentheses in case of postgres cast :: syntax. - return ", ".join([f"({key}) VALUE {value}" for key, value in pairs]) - - def as_native(self, compiler, connection, *, returning, **extra_context): - return self.as_sql( - compiler, - connection, - arg_joiner=self, - template=f"%(function)s(%(expressions)s RETURNING {returning})", - **extra_context, - ) - - def as_postgresql(self, compiler, connection, **extra_context): - # Casting keys to text is only required when using JSONB_BUILD_OBJECT - # or when using JSON_OBJECT on PostgreSQL 16+ with server-side bindings. - # This is done in all cases for consistency. - copy = self.copy() - copy.set_source_expressions( - [ - Cast(expression, TextField()) if index % 2 == 0 else expression - for index, expression in enumerate(copy.get_source_expressions()) - ] - ) - - if connection.features.is_postgresql_16: - return copy.as_native( - compiler, connection, returning="JSONB", **extra_context - ) - - return super(JSONObject, copy).as_sql( - compiler, - connection, - function="JSONB_BUILD_OBJECT", - **extra_context, - ) - - def as_oracle(self, compiler, connection, **extra_context): - return self.as_native(compiler, connection, returning="CLOB", **extra_context) - - class Least(Func): """ Return the minimum expression. diff --git a/django/db/models/functions/json.py b/django/db/models/functions/json.py new file mode 100644 index 0000000000..25c3872419 --- /dev/null +++ b/django/db/models/functions/json.py @@ -0,0 +1,64 @@ +from django.db import NotSupportedError +from django.db.models.expressions import Func, Value +from django.db.models.fields import TextField +from django.db.models.fields.json import JSONField +from django.db.models.functions import Cast + + +class JSONObject(Func): + function = "JSON_OBJECT" + output_field = JSONField() + + def __init__(self, **fields): + expressions = [] + for key, value in fields.items(): + expressions.extend((Value(key), value)) + super().__init__(*expressions) + + def as_sql(self, compiler, connection, **extra_context): + if not connection.features.has_json_object_function: + raise NotSupportedError( + "JSONObject() is not supported on this database backend." + ) + return super().as_sql(compiler, connection, **extra_context) + + def join(self, args): + pairs = zip(args[::2], args[1::2], strict=True) + # Wrap 'key' in parentheses in case of postgres cast :: syntax. + return ", ".join([f"({key}) VALUE {value}" for key, value in pairs]) + + def as_native(self, compiler, connection, *, returning, **extra_context): + return self.as_sql( + compiler, + connection, + arg_joiner=self, + template=f"%(function)s(%(expressions)s RETURNING {returning})", + **extra_context, + ) + + def as_postgresql(self, compiler, connection, **extra_context): + # Casting keys to text is only required when using JSONB_BUILD_OBJECT + # or when using JSON_OBJECT on PostgreSQL 16+ with server-side bindings. + # This is done in all cases for consistency. + copy = self.copy() + copy.set_source_expressions( + [ + Cast(expression, TextField()) if index % 2 == 0 else expression + for index, expression in enumerate(copy.get_source_expressions()) + ] + ) + + if connection.features.is_postgresql_16: + return copy.as_native( + compiler, connection, returning="JSONB", **extra_context + ) + + return super(JSONObject, copy).as_sql( + compiler, + connection, + function="JSONB_BUILD_OBJECT", + **extra_context, + ) + + def as_oracle(self, compiler, connection, **extra_context): + return self.as_native(compiler, connection, returning="CLOB", **extra_context) diff --git a/docs/ref/models/database-functions.txt b/docs/ref/models/database-functions.txt index eb08e160f7..f23585739e 100644 --- a/docs/ref/models/database-functions.txt +++ b/docs/ref/models/database-functions.txt @@ -163,31 +163,6 @@ and ``comment.modified``. The PostgreSQL behavior can be emulated using ``Coalesce`` if you know a sensible minimum value to provide as a default. -``JSONObject`` --------------- - -.. class:: JSONObject(**fields) - -Takes a list of key-value pairs and returns a JSON object containing those -pairs. - -Usage example: - -.. code-block:: pycon - - >>> from django.db.models import F - >>> from django.db.models.functions import JSONObject, Lower - >>> Author.objects.create(name="Margaret Smith", alias="msmith", age=25) - >>> author = Author.objects.annotate( - ... json_object=JSONObject( - ... name=Lower("name"), - ... alias="alias", - ... age=F("age") * 2, - ... ) - ... ).get() - >>> author.json_object - {'name': 'margaret smith', 'alias': 'msmith', 'age': 50} - ``Least`` --------- @@ -861,6 +836,36 @@ that deal with time-parts can be used with ``TimeField``: 2014-06-16 00:00:00+10:00 2 2016-01-01 04:00:00+11:00 1 +.. _json-functions: + +JSON Functions +============== + +``JSONObject`` +-------------- + +.. class:: JSONObject(**fields) + +Takes a list of key-value pairs and returns a JSON object containing those +pairs. + +Usage example: + +.. code-block:: pycon + + >>> from django.db.models import F + >>> from django.db.models.functions import JSONObject, Lower + >>> Author.objects.create(name="Margaret Smith", alias="msmith", age=25) + >>> author = Author.objects.annotate( + ... json_object=JSONObject( + ... name=Lower("name"), + ... alias="alias", + ... age=F("age") * 2, + ... ) + ... ).get() + >>> author.json_object + {'name': 'margaret smith', 'alias': 'msmith', 'age': 50} + .. _math-functions: Math Functions diff --git a/tests/db_functions/json/__init__.py b/tests/db_functions/json/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/db_functions/comparison/test_json_object.py b/tests/db_functions/json/test_json_object.py similarity index 100% rename from tests/db_functions/comparison/test_json_object.py rename to tests/db_functions/json/test_json_object.py