From c991602ce5798385261381025c06698d7fd30dc5 Mon Sep 17 00:00:00 2001 From: Albert Defler Date: Wed, 14 Feb 2024 17:34:00 +0000 Subject: [PATCH] Fixed #34060 -- Fixed migrations crash when adding check constraints with JSONField __exact lookup on Oracle. --- django/db/backends/oracle/operations.py | 7 +++-- django/db/backends/postgresql/features.py | 7 +++++ django/db/models/fields/json.py | 5 ++++ tests/constraints/tests.py | 14 ++++++++++ tests/schema/tests.py | 34 +++++++++++++++++++++++ 5 files changed, 64 insertions(+), 3 deletions(-) diff --git a/django/db/backends/oracle/operations.py b/django/db/backends/oracle/operations.py index 541128ec50..4f4658299d 100644 --- a/django/db/backends/oracle/operations.py +++ b/django/db/backends/oracle/operations.py @@ -347,9 +347,10 @@ END; def lookup_cast(self, lookup_type, internal_type=None): if lookup_type in ("iexact", "icontains", "istartswith", "iendswith"): return "UPPER(%s)" - if ( - lookup_type != "isnull" and internal_type in ("BinaryField", "TextField") - ) or (lookup_type == "exact" and internal_type == "JSONField"): + if lookup_type != "isnull" and internal_type in ( + "BinaryField", + "TextField", + ): return "DBMS_LOB.SUBSTR(%s)" return "%s" diff --git a/django/db/backends/postgresql/features.py b/django/db/backends/postgresql/features.py index 7bcc356407..5880390827 100644 --- a/django/db/backends/postgresql/features.py +++ b/django/db/backends/postgresql/features.py @@ -106,6 +106,13 @@ class DatabaseFeatures(BaseDatabaseFeatures): "test_group_by_nested_expression_with_params", } ) + if not is_psycopg3: + expected_failures.update( + { + "constraints.tests.CheckConstraintTests." + "test_validate_jsonfield_exact", + } + ) return expected_failures @cached_property diff --git a/django/db/models/fields/json.py b/django/db/models/fields/json.py index 571e6e79f3..5a9c02e032 100644 --- a/django/db/models/fields/json.py +++ b/django/db/models/fields/json.py @@ -310,6 +310,11 @@ class JSONExact(lookups.Exact): rhs %= tuple(func) return rhs, rhs_params + def as_oracle(self, compiler, connection): + lhs, lhs_params = self.process_lhs(compiler, connection) + rhs, rhs_params = self.process_rhs(compiler, connection) + return f"JSON_EQUAL({lhs}, {rhs})", (*lhs_params, *rhs_params) + class JSONIContains(CaseInsensitiveMixin, lookups.IContains): pass diff --git a/tests/constraints/tests.py b/tests/constraints/tests.py index 55df5975de..40fdec6c40 100644 --- a/tests/constraints/tests.py +++ b/tests/constraints/tests.py @@ -365,6 +365,20 @@ class CheckConstraintTests(TestCase): constraint_with_pk.validate(ChildModel, ChildModel(id=1, age=1)) constraint_with_pk.validate(ChildModel, ChildModel(pk=1, age=1), exclude={"pk"}) + @skipUnlessDBFeature("supports_json_field") + def test_validate_jsonfield_exact(self): + data = {"release": "5.0.2", "version": "stable"} + json_exact_constraint = models.CheckConstraint( + check=models.Q(data__version="stable"), + name="only_stable_version", + ) + json_exact_constraint.validate(JSONFieldModel, JSONFieldModel(data=data)) + + data = {"release": "5.0.2", "version": "not stable"} + msg = f"Constraint “{json_exact_constraint.name}” is violated." + with self.assertRaisesMessage(ValidationError, msg): + json_exact_constraint.validate(JSONFieldModel, JSONFieldModel(data=data)) + class UniqueConstraintTests(TestCase): @classmethod diff --git a/tests/schema/tests.py b/tests/schema/tests.py index ced3367f00..52f5f289a1 100644 --- a/tests/schema/tests.py +++ b/tests/schema/tests.py @@ -2803,6 +2803,40 @@ class SchemaTests(TransactionTestCase): DurationModel.objects.create(duration=datetime.timedelta(minutes=4)) DurationModel.objects.create(duration=datetime.timedelta(minutes=10)) + @skipUnlessDBFeature( + "supports_column_check_constraints", + "can_introspect_check_constraints", + "supports_json_field", + ) + @isolate_apps("schema") + def test_check_constraint_exact_jsonfield(self): + class JSONConstraintModel(Model): + data = JSONField() + + class Meta: + app_label = "schema" + + with connection.schema_editor() as editor: + editor.create_model(JSONConstraintModel) + self.isolated_local_models = [JSONConstraintModel] + constraint_name = "check_only_stable_version" + constraint = CheckConstraint( + check=Q(data__version="stable"), + name=constraint_name, + ) + JSONConstraintModel._meta.constraints = [constraint] + with connection.schema_editor() as editor: + editor.add_constraint(JSONConstraintModel, constraint) + constraints = self.get_constraints(JSONConstraintModel._meta.db_table) + self.assertIn(constraint_name, constraints) + with self.assertRaises(IntegrityError), atomic(): + JSONConstraintModel.objects.create( + data={"release": "5.0.2dev", "version": "dev"} + ) + JSONConstraintModel.objects.create( + data={"release": "5.0.3", "version": "stable"} + ) + @skipUnlessDBFeature( "supports_column_check_constraints", "can_introspect_check_constraints" )