Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,7 @@ answer newbie questions, and generally made Django that much better:
Ryan Rubin <ryanmrubin@gmail.com>
Ryno Mathee <rmathee@gmail.com>
Sachin Jat <sanch.jat@gmail.com>
Sage M. Abdullah <https://github.com/laymonage>
Sam Newman <http://www.magpiebrain.com/>
Sander Dijkhuis <sander.dijkhuis@gmail.com>
Sanket Saurav <sanketsaurav@gmail.com>
Expand Down
4 changes: 2 additions & 2 deletions django/contrib/postgres/aggregates/general.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.contrib.postgres.fields import ArrayField, JSONField
from django.db.models import Aggregate, Value
from django.contrib.postgres.fields import ArrayField
from django.db.models import Aggregate, JSONField, Value

from .mixins import OrderableAggMixin

Expand Down
1 change: 0 additions & 1 deletion django/contrib/postgres/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ def ready(self):
for conn in connections.all():
if conn.vendor == 'postgresql':
conn.introspection.data_types_reverse.update({
3802: 'django.contrib.postgres.fields.JSONField',
3904: 'django.contrib.postgres.fields.IntegerRangeField',
3906: 'django.contrib.postgres.fields.DecimalRangeField',
3910: 'django.contrib.postgres.fields.DateTimeRangeField',
Expand Down
202 changes: 30 additions & 172 deletions django/contrib/postgres/fields/jsonb.py
Original file line number Diff line number Diff line change
@@ -1,185 +1,43 @@
import json
import warnings

from psycopg2.extras import Json

from django.contrib.postgres import forms, lookups
from django.core import exceptions
from django.db.models import (
Field, TextField, Transform, lookups as builtin_lookups,
from django.db.models import JSONField as BuiltinJSONField
from django.db.models.fields.json import (
KeyTextTransform as BuiltinKeyTextTransform,
KeyTransform as BuiltinKeyTransform,
)
from django.db.models.fields.mixins import CheckFieldDefaultMixin
from django.utils.translation import gettext_lazy as _
from django.utils.deprecation import RemovedInDjango40Warning

__all__ = ['JSONField']


class JsonAdapter(Json):
"""
Customized psycopg2.extras.Json to allow for a custom encoder.
"""
def __init__(self, adapted, dumps=None, encoder=None):
self.encoder = encoder
super().__init__(adapted, dumps=dumps)

def dumps(self, obj):
options = {'cls': self.encoder} if self.encoder else {}
return json.dumps(obj, **options)


class JSONField(CheckFieldDefaultMixin, Field):
empty_strings_allowed = False
description = _('A JSON object')
default_error_messages = {
'invalid': _("Value must be valid JSON."),
class JSONField(BuiltinJSONField):
system_check_deprecated_details = {
'msg': (
'django.contrib.postgres.fields.JSONField is deprecated. Support '
'for it (except in historical migrations) will be removed in '
'Django 4.0.'
),
'hint': 'Use django.db.models.JSONField instead.',
'id': 'fields.W904',
}
_default_hint = ('dict', '{}')

def __init__(self, verbose_name=None, name=None, encoder=None, **kwargs):
if encoder and not callable(encoder):
raise ValueError("The encoder parameter must be a callable object.")
self.encoder = encoder
super().__init__(verbose_name, name, **kwargs)

def db_type(self, connection):
return 'jsonb'

def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
if self.encoder is not None:
kwargs['encoder'] = self.encoder
return name, path, args, kwargs

def get_transform(self, name):
transform = super().get_transform(name)
if transform:
return transform
return KeyTransformFactory(name)

def get_prep_value(self, value):
if value is not None:
return JsonAdapter(value, encoder=self.encoder)
return value

def validate(self, value, model_instance):
super().validate(value, model_instance)
options = {'cls': self.encoder} if self.encoder else {}
try:
json.dumps(value, **options)
except TypeError:
raise exceptions.ValidationError(
self.error_messages['invalid'],
code='invalid',
params={'value': value},
)

def value_to_string(self, obj):
return self.value_from_object(obj)

def formfield(self, **kwargs):
return super().formfield(**{
'form_class': forms.JSONField,
**kwargs,
})


JSONField.register_lookup(lookups.DataContains)
JSONField.register_lookup(lookups.ContainedBy)
JSONField.register_lookup(lookups.HasKey)
JSONField.register_lookup(lookups.HasKeys)
JSONField.register_lookup(lookups.HasAnyKeys)
JSONField.register_lookup(lookups.JSONExact)


class KeyTransform(Transform):
operator = '->'
nested_operator = '#>'

def __init__(self, key_name, *args, **kwargs):
class KeyTransform(BuiltinKeyTransform):
def __init__(self, *args, **kwargs):
warnings.warn(
'django.contrib.postgres.fields.jsonb.KeyTransform is deprecated '
'in favor of django.db.models.fields.json.KeyTransform.',
RemovedInDjango40Warning, stacklevel=2,
)
super().__init__(*args, **kwargs)
self.key_name = key_name

def as_sql(self, compiler, connection):
key_transforms = [self.key_name]
previous = self.lhs
while isinstance(previous, KeyTransform):
key_transforms.insert(0, previous.key_name)
previous = previous.lhs
lhs, params = compiler.compile(previous)
if len(key_transforms) > 1:
return '(%s %s %%s)' % (lhs, self.nested_operator), params + [key_transforms]
try:
lookup = int(self.key_name)
except ValueError:
lookup = self.key_name
return '(%s %s %%s)' % (lhs, self.operator), tuple(params) + (lookup,)


class KeyTextTransform(KeyTransform):
operator = '->>'
nested_operator = '#>>'
output_field = TextField()


class KeyTransformTextLookupMixin:
"""
Mixin for combining with a lookup expecting a text lhs from a JSONField
key lookup. Make use of the ->> operator instead of casting key values to
text and performing the lookup on the resulting representation.
"""
def __init__(self, key_transform, *args, **kwargs):
assert isinstance(key_transform, KeyTransform)
key_text_transform = KeyTextTransform(
key_transform.key_name, *key_transform.source_expressions, **key_transform.extra
class KeyTextTransform(BuiltinKeyTextTransform):
def __init__(self, *args, **kwargs):
warnings.warn(
'django.contrib.postgres.fields.jsonb.KeyTextTransform is '
'deprecated in favor of '
'django.db.models.fields.json.KeyTextTransform.',
RemovedInDjango40Warning, stacklevel=2,
)
super().__init__(key_text_transform, *args, **kwargs)


class KeyTransformIExact(KeyTransformTextLookupMixin, builtin_lookups.IExact):
pass


class KeyTransformIContains(KeyTransformTextLookupMixin, builtin_lookups.IContains):
pass


class KeyTransformStartsWith(KeyTransformTextLookupMixin, builtin_lookups.StartsWith):
pass


class KeyTransformIStartsWith(KeyTransformTextLookupMixin, builtin_lookups.IStartsWith):
pass


class KeyTransformEndsWith(KeyTransformTextLookupMixin, builtin_lookups.EndsWith):
pass


class KeyTransformIEndsWith(KeyTransformTextLookupMixin, builtin_lookups.IEndsWith):
pass


class KeyTransformRegex(KeyTransformTextLookupMixin, builtin_lookups.Regex):
pass


class KeyTransformIRegex(KeyTransformTextLookupMixin, builtin_lookups.IRegex):
pass


KeyTransform.register_lookup(KeyTransformIExact)
KeyTransform.register_lookup(KeyTransformIContains)
KeyTransform.register_lookup(KeyTransformStartsWith)
KeyTransform.register_lookup(KeyTransformIStartsWith)
KeyTransform.register_lookup(KeyTransformEndsWith)
KeyTransform.register_lookup(KeyTransformIEndsWith)
KeyTransform.register_lookup(KeyTransformRegex)
KeyTransform.register_lookup(KeyTransformIRegex)


class KeyTransformFactory:

def __init__(self, key_name):
self.key_name = key_name

def __call__(self, *args, **kwargs):
return KeyTransform(self.key_name, *args, **kwargs)
super().__init__(*args, **kwargs)
69 changes: 11 additions & 58 deletions django/contrib/postgres/forms/jsonb.py
Original file line number Diff line number Diff line change
@@ -1,63 +1,16 @@
import json
import warnings

from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from django.forms import JSONField as BuiltinJSONField
from django.utils.deprecation import RemovedInDjango40Warning

__all__ = ['JSONField']


class InvalidJSONInput(str):
pass


class JSONString(str):
pass


class JSONField(forms.CharField):
default_error_messages = {
'invalid': _('“%(value)s” value must be valid JSON.'),
}
widget = forms.Textarea

def to_python(self, value):
if self.disabled:
return value
if value in self.empty_values:
return None
elif isinstance(value, (list, dict, int, float, JSONString)):
return value
try:
converted = json.loads(value)
except json.JSONDecodeError:
raise ValidationError(
self.error_messages['invalid'],
code='invalid',
params={'value': value},
)
if isinstance(converted, str):
return JSONString(converted)
else:
return converted

def bound_data(self, data, initial):
if self.disabled:
return initial
try:
return json.loads(data)
except json.JSONDecodeError:
return InvalidJSONInput(data)

def prepare_value(self, value):
if isinstance(value, InvalidJSONInput):
return value
return json.dumps(value)

def has_changed(self, initial, data):
if super().has_changed(initial, data):
return True
# For purposes of seeing whether something has changed, True isn't the
# same as 1 and the order of keys doesn't matter.
data = self.to_python(data)
return json.dumps(initial, sort_keys=True) != json.dumps(data, sort_keys=True)
class JSONField(BuiltinJSONField):
def __init__(self, *args, **kwargs):
warnings.warn(
'django.contrib.postgres.forms.JSONField is deprecated in favor '
'of django.forms.JSONField.',
RemovedInDjango40Warning, stacklevel=2,
)
super().__init__(*args, **kwargs)
11 changes: 1 addition & 10 deletions django/contrib/postgres/lookups.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from django.db.models import Transform
from django.db.models.lookups import Exact, PostgresOperatorLookup
from django.db.models.lookups import PostgresOperatorLookup

from .search import SearchVector, SearchVectorExact, SearchVectorField

Expand Down Expand Up @@ -58,12 +58,3 @@ def process_lhs(self, qn, connection):
class TrigramSimilar(PostgresOperatorLookup):
lookup_name = 'trigram_similar'
postgres_operator = '%%'


class JSONExact(Exact):
can_use_none_as_rhs = True

def process_rhs(self, compiler, connection):
result = super().process_rhs(compiler, connection)
# Treat None lookup values as null.
return ("'null'", []) if result == ('%s', [None]) else result
9 changes: 9 additions & 0 deletions django/db/backends/base/features.py
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,15 @@ class BaseDatabaseFeatures:
# Does the backend support boolean expressions in the SELECT clause?
supports_boolean_expr_in_select_clause = True

# Does the backend support JSONField?
supports_json_field = True
# Can the backend introspect a JSONField?
can_introspect_json_field = True
# Does the backend support primitives in JSONField?
supports_primitives_in_json_field = True
# Is there a true datatype for JSON?
has_native_json_field = False

def __init__(self, connection):
self.connection = connection

Expand Down
7 changes: 7 additions & 0 deletions django/db/backends/base/operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,13 @@ def time_extract_sql(self, lookup_type, field_name):
"""
return self.date_extract_sql(lookup_type, field_name)

def json_cast_text_sql(self, field_name):
"""Return the SQL to cast a JSON value to text value."""
raise NotImplementedError(
'subclasses of BaseDatabaseOperations may require a '
'json_cast_text_sql() method'
)

def deferrable_sql(self):
"""
Return the SQL to make a constraint "initially deferred" during a
Expand Down
Loading