百度 此外,恒大农牧官方电商平台恒大优选已经正式上线,消费者通过手机轻松一点,或通过购买精品兑换卡,微信轻松一扫,就可享受到恒大农牧电商平台或卡兑换服务团队送货到家的周到服务。

Opened 16个月 ago

Last modified 3周 ago

#35381 assigned New feature

Provide `JSONNull` expression to represent JSON `null` value

汇报人: Olivier Tabone 属主: Clifford Gama
组件: Database layer (models, ORM) 版本: dev
严重性: Normal 关键词:
抄送: David Sanders, Simon Charette, Mariusz Felisiak, David Wobrock, Sage Abdullah, Clifford Gama, Adam Johnson Triage Stage: Accepted
Has patch: Needs documentation:
Needs tests: Patch needs improvement:
Easy pickings: UI/UX:

描述 (最后由 Natalia Bidart 修改)

From Simon Charette in comment:3, in reference to the issues between SQL's NULLL and JSON's null:

In order to finally solve this null ambiguity problem we should:

  1. Introduce a JSONNull expression to disambiguate between the two. It would also allow the creation of model instances with a JSON null which is convoluted today as it requires models.Value(None, models.JSONField()) to be used.
  2. Deprecate filter(jsonfield=None) meaning JSON null by requiring JSONNull to be used instead. Should we only do this at the top level to still allow jsonfield__key=None to filter against null keys? An alternative would be to introduce a __jsonnull lookup.

The good news is that Django makes it very hard to store JSON null in the first place since #34754 so this kind of constraints should be rarely needed.

Old description

Regression is still present in 5.0 and main branch

given a model defined as follow

from django.db import models


class JSONFieldModel(models.Model):
    data = models.JSONField(null=True, blank=True, default=None)

    class Meta:
        required_db_features = {"supports_json_field"}
        constraints = [
            models.CheckConstraint(
                check=~models.Q(data=models.Value(None, models.JSONField())),
                name="json_data_cant_be_json_null",
            ),
        ]

the following test works in django 4.1.13, fails in later version

from django.test import TestCase, skipUnlessDBFeature

from .models import JSONFieldModel


class CheckConstraintTests(TestCase):
    @skipUnlessDBFeature("supports_json_field")
    def test_full_clean_on_null_value(self):
        instance = JSONFieldModel.objects.create(data=None)  # data = SQL Null value
        instance.full_clean()

tests/runtests.py json_null_tests                    
======================================================================
ERROR: test_full_clean_on_null_value (json_null_tests.tests.CheckConstraintTests.test_full_clean_on_null_value)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/olivier/dev/django.dev/olibook/django/django/test/testcases.py", line 1428, in skip_wrapper
    return test_func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/olivier/dev/django.dev/olibook/django/tests/json_null_tests/tests.py", line 13, in test_full_clean_on_null_value
    instance.full_clean()
  File "/Users/olivier/dev/django.dev/olibook/django/django/db/models/base.py", line 1504, in full_clean
    raise ValidationError(errors)
django.core.exceptions.ValidationError: {'__all__': ['Constraint “json_data_cant_be_json_null” is violated.']}

----------------------------------------------------------------------

Attached a patch that will create run json_null_tests folder in django's tests directories. Test can be run with

tests/runtests.py json_null_tests

NOTE : in django 5.1 the CheckConstraint.check parameter as been renamed to CheckConstraint.condition.

I ran a git bisect on this test case:

8fcb9f1f106cf60d953d88aeaa412cc625c60029 is bad
e14d08cd894e9d91cb5d9f44ba7532c1a223f458 is good

git bisect run tests/runtests.py json_null_tests

5c23d9f0c32f166c81ecb6f3f01d5077a6084318 is identified as first bad commit

the issue appears with both sqlite and postgres backends

A few words on what we I'm trying to achieve (some context on the regression):

The json_data_cant_be_json_null aims to prevent having JsonModel.data = None (SQL Null) and JSONModel.data = Value(None, JSONField()) (JSON null) values in the database. These leads to unwanted behaviors when filtering on the null value, and I settled on representing the absence of value with SQL Null and not JSON null.

The app was running in django 4.1 and I'm upgrading it to later django version. That's how the issue appeared.

附件 (1)

json_null_tests.patch (1.3 KB ) - added by Olivier Tabone 16个月 ago.

Download all attachments as: .zip

变更历史 (18)

by Olivier Tabone, 16个月 ago

Attachment: json_null_tests.patch added

comment:1 by Natalia Bidart, 16个月 ago

Triage Stage: UnreviewedAccepted
抄送: David Sanders Simon Charette Mariusz Felisiak added
类型: UncategorizedBug

I think this is related to #34754, and I originally thought that this was another consequence of what Simon said there: I think the fundamental problem here is that:

MyModel.objects.create(values=None)  # Creates an object with a SQL NULL
MyModel.objects.filter(values=None)  # Filters for objects with JSON NULL

but then I ran the proposed test with --debug-sql (and postgres) and noticed that the check code is casting the value in the database to ::JSONB independently of whether the value is None or not:

INSERT INTO "ticket_35381_jsonfieldmodel" ("data")
VALUES (NULL) RETURNING "ticket_35381_jsonfieldmodel"."id";

args=(NONE,);

ALIAS=DEFAULT (0.000)
SELECT 1 AS "_check"
WHERE COALESCE((NOT ('null'::JSONB = 'null'::JSONB)), TRUE);

args=(Int4(1),
      Jsonb(NONE),
      Jsonb(NONE),
      TRUE);

ALIAS=DEFAULT

So I kept digging and bisected the "bad" commit to 5c23d9f0c32f166c81ecb6f3f01d5077a6084318, and something like this makes the test pass:

  • django/db/models/fields/json.py

    diff --git a/django/db/models/fields/json.py b/django/db/models/fields/json.py
    index 1b219e620c..9bd6c4b252 100644
    a b class JSONField(CheckFieldDefaultMixin, Field):  
    103103            value.output_field, JSONField
    104104        ):
    105105            value = value.value
    106         elif hasattr(value, "as_sql"):
     106        elif value is None or hasattr(value, "as_sql"):
    107107            return value
    108108        return connection.ops.adapt_json_value(value, self.encoder)

it makes the test model_fields.test_jsonfield.TestSaveLoad.test_json_null_different_from_sql_null fails for the check on the query:

NullableJSONModel.objects.filter(value=Value(None, JSONField())) ==  [json_null]

So further investigation is needed to come up with an acceptable solution.

Last edited 16个月 ago by Natalia Bidart (上一个) (差异)

comment:2 by Natalia Bidart, 16个月 ago

Ticket #35167 is related but I don't think is a dupe.

Last edited 16个月 ago by Natalia Bidart (上一个) (差异)

comment:3 by Simon Charette, 15个月 ago

I've looked into it as well and #35167 only provides a partial solution here IMO as it would require the Q.check logic to also pass around for_save to activate the code path it introduces.

I'm starting to believe that in order to finally solve this null ambiguity problem we should

  1. Introduce a JSONNull expression to disambigutate between the two. It would also allow the creation of model instances with a JSON null which is convoluated today as it requires models.Value(None, models.JSONField()) to be used.
  2. Deprecate filter(jsonfield=None) meaning JSON null by requiring JSONNull to be used instead. Should we only do this at the top level to still allow jsonfield__key=None to filter against null keys? An alternative would be to introduce a __jsonnull lookup.

The good news is that Django makes it very hard to store JSON null in the first place since #34754 so this kind of constraints should be rarely needed.

comment:4 by Natalia Bidart, 15个月 ago

Thank you Simon. In my view, and in an ideal situation, we shouldn't allow "anywhere" to use None as the json's null. I think we should reserve None only to represent SQL's NULL, and have a representation for the (string) json's null (I understand this matches the new JSONNull expression you are proposing).

Should we only do this at the top level to still allow jsonfield__key=None to filter against null keys?

I would say no, because it then gets very confusing.

An alternative would be to introduce a __jsonnull lookup.

Would this lookup allow further lookups to be chained?

comment:5 by Simon Charette, 15个月 ago

I think we should reserve None only to represent SQL's NULL, and have a representation for the (string) json's null (I understand this matches the new JSONNull expression you are proposing).

Yes it matches the JSONNull proposal. If we take away the ability to use None for filtering against JSON null we should provide an alternative.

The deprecation could look like

  1. Introduce JSONNull
  2. Make filter(jsonfield=None) raise a deprecation warning pointing at either using filter(jsonfield__isnull=True) or filter(jsonfield=JSONNull)
  3. At the end of the deprecation period switch filter(jsonfield=None) to mean filter(jsonfield__isnull=True) like on all other fields

It leaves the problem of having JSON null not surviving a round trip to the database as both SQL NULL and json.loads("null") are turned into Python None but that's a different issue that can be addressed with a specialized decoder if users require it.

Would this lookup allow further lookups to be chained?

It could or not, what gets returned from a lookup can be prevent further chaining if we want.

Last edited 15个月 ago by Simon Charette (上一个) (差异)

comment:6 by Mohammad Salehi, 12个月 ago

Hello, if you're okay with it, I would like to start working on this ticket and submit a PR to address the issue.

in reply to:  6 comment:7 by David Wobrock, 12个月 ago

抄送: David Wobrock added

Replying to Mohammad Salehi:

Hello, if you're okay with it, I would like to start working on this ticket and submit a PR to address the issue.

Of course! Feel free to assign the ticket to you and open a patch.
Bear in mind the suggested approach from the discussions above :)

comment:8 by devday, 11个月 ago

属主: nobody 改变为 devday
状态: newassigned

comment:9 by wookkl, 3个月 ago

属主: devday 改变为 wookkl

comment:10 by Natalia Bidart, 2个月 ago

抄送: Sage Abdullah added

In the context of investigating #36418, I have re-reviewed the different tickets that we have related to confusing behaviours of SQL's NULL vs JSON's string null, and I think this ticket should be edited in summary and description to match the New Feature described in comment:5 (or alternatively, close this one as invalid and open a new ticket).

comment:11 by Clifford Gama, 2个月 ago

抄送: Clifford Gama added

comment:12 by Adam Johnson, 2个月 ago

抄送: Adam Johnson added

comment:13 by wookkl, 7周 ago

属主: wookkl removed
状态: assignednew

comment:14 by Natalia Bidart, 6周 ago

描述: 修改了 (差异)
概述: Regression on json null value constraints in django 4.2Provide `JSONNull` expression to represent JSON `null` value
版本: 5.0dev
类型: BugNew feature

comment:15 by Clifford Gama, 5周 ago

属主: 设置为 Clifford Gama
状态: newassigned

comment:16 by Jacob Walls, 3周 ago

#36508 was a dupe. We'll want to highlight this deprecation prominently in the release notes, since unlike top-level JSON nulls, JSON nulls in key/value pairs can be expected to appear more frequently.

comment:17 by Simon Charette, 3周 ago

Another edge case of the dual interpretation of None ?when storing and querying JSONField (SQL NULL on persisting and filter(json_field=None) on filtering) that came up at work today is how it breaks get_or_create expectations that the filter and creation value will match.

It means that doing

Foo.objects.get_or_create(bar=123, json_field=None)

will perform

SELECT * FROM foo WHERE bar = 123 AND json_field = 'null'::jsonb
INSERT INTO foo (bar, json_field) VALUES (123, NULL) 

using JSONNull() (or it's closet equivalent today being Value(None, JSONField())) would allows for JSON null to be used in both cases (querying and storing) but there are no ways to specify that SQL NULL must be used in both cases which is a supporting point for deprecating filter(json_field=None) meaning JSON null. Here is ?an attempt at getting it IS NULL to be used for comparison and persistence.

Last edited 3周 ago by Simon Charette (上一个) (差异)
Note: See TracTickets for help on using tickets.
Back to Top