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
|
||||
# {'d': [{'f': 'g'}]}?
|
||||
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?
|
||||
supports_collation_on_charfield = True
|
||||
|
|
|
@ -94,3 +94,8 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
|||
return False
|
||||
raise
|
||||
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
|
||||
|
||||
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 (
|
||||
Extract, ExtractDay, ExtractHour, ExtractIsoWeekDay, ExtractIsoYear,
|
||||
ExtractMinute, ExtractMonth, ExtractQuarter, ExtractSecond, ExtractWeek,
|
||||
|
@ -22,7 +24,7 @@ from .window import (
|
|||
|
||||
__all__ = [
|
||||
# comparison and conversion
|
||||
'Cast', 'Coalesce', 'Collate', 'Greatest', 'Least', 'NullIf',
|
||||
'Cast', 'Coalesce', 'Collate', 'Greatest', 'JSONObject', 'Least', 'NullIf',
|
||||
# datetime
|
||||
'Extract', 'ExtractDay', 'ExtractHour', 'ExtractMinute', 'ExtractMonth',
|
||||
'ExtractQuarter', 'ExtractSecond', 'ExtractWeek', 'ExtractIsoWeekDay',
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
"""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.json import JSONField
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
Return the minimum expression.
|
||||
|
|
|
@ -148,6 +148,29 @@ 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)
|
||||
|
||||
.. 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``
|
||||
---------
|
||||
|
||||
|
|
|
@ -367,6 +367,8 @@ Models
|
|||
block exits without errors. A nested atomic block marked as durable will
|
||||
raise a ``RuntimeError``.
|
||||
|
||||
* Added the :class:`~django.db.models.functions.JSONObject` database function.
|
||||
|
||||
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