Fixed #32179 -- Added JSONObject database function.

This commit is contained in:
Artur Beltsov 2020-11-08 12:52:34 +05:00 committed by Mariusz Felisiak
parent adb40d217e
commit 48b4bae983
8 changed files with 161 additions and 2 deletions

View File

@ -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

View File

@ -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,)

View File

@ -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'))

View File

@ -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',

View File

@ -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.

View File

@ -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``
---------

View File

@ -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
~~~~~~~~~~

View File

@ -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()