Fixed #32179 -- Added JSONObject database function.
This commit is contained in:
parent
adb40d217e
commit
48b4bae983
|
@ -306,6 +306,8 @@ class BaseDatabaseFeatures:
|
||||||
# Does value__d__contains={'f': 'g'} (without a list around the dict) match
|
# Does value__d__contains={'f': 'g'} (without a list around the dict) match
|
||||||
# {'d': [{'f': 'g'}]}?
|
# {'d': [{'f': 'g'}]}?
|
||||||
json_key_contains_list_matching_requires_list = False
|
json_key_contains_list_matching_requires_list = False
|
||||||
|
# Does the backend support JSONObject() database function?
|
||||||
|
has_json_object_function = True
|
||||||
|
|
||||||
# Does the backend support column collations?
|
# Does the backend support column collations?
|
||||||
supports_collation_on_charfield = True
|
supports_collation_on_charfield = True
|
||||||
|
|
|
@ -94,3 +94,8 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
||||||
return False
|
return False
|
||||||
raise
|
raise
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def has_json_object_function(self):
|
||||||
|
# Oracle < 18 supports JSON_OBJECT() but it's not fully functional.
|
||||||
|
return self.connection.oracle_version >= (18,)
|
||||||
|
|
|
@ -79,3 +79,4 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
can_introspect_json_field = property(operator.attrgetter('supports_json_field'))
|
can_introspect_json_field = property(operator.attrgetter('supports_json_field'))
|
||||||
|
has_json_object_function = property(operator.attrgetter('supports_json_field'))
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
from .comparison import Cast, Coalesce, Collate, Greatest, Least, NullIf
|
from .comparison import (
|
||||||
|
Cast, Coalesce, Collate, Greatest, JSONObject, Least, NullIf,
|
||||||
|
)
|
||||||
from .datetime import (
|
from .datetime import (
|
||||||
Extract, ExtractDay, ExtractHour, ExtractIsoWeekDay, ExtractIsoYear,
|
Extract, ExtractDay, ExtractHour, ExtractIsoWeekDay, ExtractIsoYear,
|
||||||
ExtractMinute, ExtractMonth, ExtractQuarter, ExtractSecond, ExtractWeek,
|
ExtractMinute, ExtractMonth, ExtractQuarter, ExtractSecond, ExtractWeek,
|
||||||
|
@ -22,7 +24,7 @@ from .window import (
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
# comparison and conversion
|
# comparison and conversion
|
||||||
'Cast', 'Coalesce', 'Collate', 'Greatest', 'Least', 'NullIf',
|
'Cast', 'Coalesce', 'Collate', 'Greatest', 'JSONObject', 'Least', 'NullIf',
|
||||||
# datetime
|
# datetime
|
||||||
'Extract', 'ExtractDay', 'ExtractHour', 'ExtractMinute', 'ExtractMonth',
|
'Extract', 'ExtractDay', 'ExtractHour', 'ExtractMinute', 'ExtractMonth',
|
||||||
'ExtractQuarter', 'ExtractSecond', 'ExtractWeek', 'ExtractIsoWeekDay',
|
'ExtractQuarter', 'ExtractSecond', 'ExtractWeek', 'ExtractIsoWeekDay',
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
"""Database functions that do comparisons or type conversions."""
|
"""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.expressions import Func, Value
|
||||||
|
from django.db.models.fields.json import JSONField
|
||||||
from django.utils.regex_helper import _lazy_re_compile
|
from django.utils.regex_helper import _lazy_re_compile
|
||||||
|
|
||||||
|
|
||||||
|
@ -112,6 +114,46 @@ class Greatest(Func):
|
||||||
return super().as_sqlite(compiler, connection, function='MAX', **extra_context)
|
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 as_postgresql(self, compiler, connection, **extra_context):
|
||||||
|
return self.as_sql(
|
||||||
|
compiler,
|
||||||
|
connection,
|
||||||
|
function='JSONB_BUILD_OBJECT',
|
||||||
|
**extra_context,
|
||||||
|
)
|
||||||
|
|
||||||
|
def as_oracle(self, compiler, connection, **extra_context):
|
||||||
|
class ArgJoiner:
|
||||||
|
def join(self, args):
|
||||||
|
args = [' VALUE '.join(arg) for arg in zip(args[::2], args[1::2])]
|
||||||
|
return ', '.join(args)
|
||||||
|
|
||||||
|
return self.as_sql(
|
||||||
|
compiler,
|
||||||
|
connection,
|
||||||
|
arg_joiner=ArgJoiner(),
|
||||||
|
template='%(function)s(%(expressions)s RETURNING CLOB)',
|
||||||
|
**extra_context,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Least(Func):
|
class Least(Func):
|
||||||
"""
|
"""
|
||||||
Return the minimum expression.
|
Return the minimum expression.
|
||||||
|
|
|
@ -148,6 +148,29 @@ and ``comment.modified``.
|
||||||
The PostgreSQL behavior can be emulated using ``Coalesce`` if you know
|
The PostgreSQL behavior can be emulated using ``Coalesce`` if you know
|
||||||
a sensible minimum value to provide as a default.
|
a sensible minimum value to provide as a default.
|
||||||
|
|
||||||
|
``JSONObject``
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. class:: JSONObject(**fields)
|
||||||
|
|
||||||
|
.. versionadded:: 3.2
|
||||||
|
|
||||||
|
Takes a list of key-value pairs and returns a JSON object containing those
|
||||||
|
pairs.
|
||||||
|
|
||||||
|
Usage example::
|
||||||
|
|
||||||
|
>>> 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``
|
``Least``
|
||||||
---------
|
---------
|
||||||
|
|
||||||
|
|
|
@ -367,6 +367,8 @@ Models
|
||||||
block exits without errors. A nested atomic block marked as durable will
|
block exits without errors. A nested atomic block marked as durable will
|
||||||
raise a ``RuntimeError``.
|
raise a ``RuntimeError``.
|
||||||
|
|
||||||
|
* Added the :class:`~django.db.models.functions.JSONObject` database function.
|
||||||
|
|
||||||
Pagination
|
Pagination
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
from django.db import NotSupportedError
|
||||||
|
from django.db.models import F, Value
|
||||||
|
from django.db.models.functions import JSONObject, Lower
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.testcases import skipIfDBFeature, skipUnlessDBFeature
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from ..models import Article, Author
|
||||||
|
|
||||||
|
|
||||||
|
@skipUnlessDBFeature('has_json_object_function')
|
||||||
|
class JSONObjectTests(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpTestData(cls):
|
||||||
|
Author.objects.create(name='Ivan Ivanov', alias='iivanov')
|
||||||
|
|
||||||
|
def test_empty(self):
|
||||||
|
obj = Author.objects.annotate(json_object=JSONObject()).first()
|
||||||
|
self.assertEqual(obj.json_object, {})
|
||||||
|
|
||||||
|
def test_basic(self):
|
||||||
|
obj = Author.objects.annotate(json_object=JSONObject(name='name')).first()
|
||||||
|
self.assertEqual(obj.json_object, {'name': 'Ivan Ivanov'})
|
||||||
|
|
||||||
|
def test_expressions(self):
|
||||||
|
obj = Author.objects.annotate(json_object=JSONObject(
|
||||||
|
name=Lower('name'),
|
||||||
|
alias='alias',
|
||||||
|
goes_by='goes_by',
|
||||||
|
salary=Value(30000.15),
|
||||||
|
age=F('age') * 2,
|
||||||
|
)).first()
|
||||||
|
self.assertEqual(obj.json_object, {
|
||||||
|
'name': 'ivan ivanov',
|
||||||
|
'alias': 'iivanov',
|
||||||
|
'goes_by': None,
|
||||||
|
'salary': 30000.15,
|
||||||
|
'age': 60,
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_nested_json_object(self):
|
||||||
|
obj = Author.objects.annotate(json_object=JSONObject(
|
||||||
|
name='name',
|
||||||
|
nested_json_object=JSONObject(
|
||||||
|
alias='alias',
|
||||||
|
age='age',
|
||||||
|
),
|
||||||
|
)).first()
|
||||||
|
self.assertEqual(obj.json_object, {
|
||||||
|
'name': 'Ivan Ivanov',
|
||||||
|
'nested_json_object': {
|
||||||
|
'alias': 'iivanov',
|
||||||
|
'age': 30,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_nested_empty_json_object(self):
|
||||||
|
obj = Author.objects.annotate(json_object=JSONObject(
|
||||||
|
name='name',
|
||||||
|
nested_json_object=JSONObject(),
|
||||||
|
)).first()
|
||||||
|
self.assertEqual(obj.json_object, {
|
||||||
|
'name': 'Ivan Ivanov',
|
||||||
|
'nested_json_object': {},
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_textfield(self):
|
||||||
|
Article.objects.create(
|
||||||
|
title='The Title',
|
||||||
|
text='x' * 4000,
|
||||||
|
written=timezone.now(),
|
||||||
|
)
|
||||||
|
obj = Article.objects.annotate(json_object=JSONObject(text=F('text'))).first()
|
||||||
|
self.assertEqual(obj.json_object, {'text': 'x' * 4000})
|
||||||
|
|
||||||
|
|
||||||
|
@skipIfDBFeature('has_json_object_function')
|
||||||
|
class JSONObjectNotSupportedTests(TestCase):
|
||||||
|
def test_not_supported(self):
|
||||||
|
msg = 'JSONObject() is not supported on this database backend.'
|
||||||
|
with self.assertRaisesMessage(NotSupportedError, msg):
|
||||||
|
Author.objects.annotate(json_object=JSONObject()).get()
|
Loading…
Reference in New Issue