#35712: CheckConstraint with RawSQL breaks ModelForm when inside
transaction.atomic()
-----------------------+-----------------------------------------
     Reporter:  a-p-f  |                     Type:  Bug
       Status:  new    |                Component:  Uncategorized
      Version:  5.1    |                 Severity:  Normal
     Keywords:         |             Triage Stage:  Unreviewed
    Has patch:  0      |      Needs documentation:  0
  Needs tests:  0      |  Patch needs improvement:  0
Easy pickings:  0      |                    UI/UX:  0
-----------------------+-----------------------------------------
 When a model uses a `CheckConstraint` with `RawSQL`, trying to save a
 `ModelForm` for that model inside a `transaction.atomic()` block fails.

 == Project Setup
 settings.py:
 {{{

 INSTALLED_APPS = [
     "test_app",
     ...
 ]
 DATABASES = {
     "default": {
         "ENGINE": "django.db.backends.postgresql",
         ...
     }
 }
 }}}

 test_app/models.py (''the Bar model has a CheckConstraint using RawSQL''):
 {{{
 from django.db import models
 from django.db.models.expressions import ExpressionWrapper, RawSQL

 class Bar(models.Model):
     a = models.IntegerField(null=True)

     class Meta:
         constraints = [
             models.CheckConstraint(
                 name="a_is_1",
                 check=ExpressionWrapper(
                     RawSQL( "a = 1", []),
                     output_field=models.BooleanField(),
                 ),
             ),
         ]
 }}}

 test_app/management_commands/form_test.py (''a ModelForm is used to create
 a Bar instance''):
 {{{
 from django import forms
 from django.core.management import BaseCommand
 from django.db import transaction

 from test_app.models import Bar

 class BarForm(forms.ModelForm):
     class Meta:
         model = Bar
         fields = ["a"]

 class Command(BaseCommand):
     def handle(self, **options):
         with transaction.atomic():
             form = BarForm({"a": 1})
             form.save()
 }}}

 == Test Procedure

 {{{
 >>> python manage.py form_test
 }}}

 == Expected Outcome
 The code runs without any exceptions, and a new Bar instance is created.

 == Actual Outcome
 `form.save()`  raises `django.db.utils.InternalError`
 {{{
 Traceback (most recent call last):
   File "/Users/alex/projects/django_constraint_issue/venv/lib/python3.10
 /site-packages/django/db/backends/utils.py", line 105, in _execute
     return self.cursor.execute(sql, params)
 psycopg2.errors.InFailedSqlTransaction: current transaction is aborted,
 commands ignored until end of transaction block


 The above exception was the direct cause of the following exception:

 Traceback (most recent call last):
   File "/Users/alex/projects/django_constraint_issue/manage.py", line 22,
 in <module>
     main()
   File "/Users/alex/projects/django_constraint_issue/manage.py", line 18,
 in main
     execute_from_command_line(sys.argv)
   File "/Users/alex/projects/django_constraint_issue/venv/lib/python3.10
 /site-packages/django/core/management/__init__.py", line 442, in
 execute_from_command_line
     utility.execute()
   File "/Users/alex/projects/django_constraint_issue/venv/lib/python3.10
 /site-packages/django/core/management/__init__.py", line 436, in execute
     self.fetch_command(subcommand).run_from_argv(self.argv)
   File "/Users/alex/projects/django_constraint_issue/venv/lib/python3.10
 /site-packages/django/core/management/base.py", line 413, in run_from_argv
     self.execute(*args, **cmd_options)
   File "/Users/alex/projects/django_constraint_issue/venv/lib/python3.10
 /site-packages/django/core/management/base.py", line 459, in execute
     output = self.handle(*args, **options)
   File
 
"/Users/alex/projects/django_constraint_issue/test_app/management/commands/form_test.py",
 line 18, in handle
     form.save()
   File "/Users/alex/projects/django_constraint_issue/venv/lib/python3.10
 /site-packages/django/forms/models.py", line 554, in save
     self.instance.save()
   File "/Users/alex/projects/django_constraint_issue/venv/lib/python3.10
 /site-packages/django/db/models/base.py", line 891, in save
     self.save_base(
   File "/Users/alex/projects/django_constraint_issue/venv/lib/python3.10
 /site-packages/django/db/models/base.py", line 997, in save_base
     updated = self._save_table(
   File "/Users/alex/projects/django_constraint_issue/venv/lib/python3.10
 /site-packages/django/db/models/base.py", line 1160, in _save_table
     results = self._do_insert(
   File "/Users/alex/projects/django_constraint_issue/venv/lib/python3.10
 /site-packages/django/db/models/base.py", line 1201, in _do_insert
     return manager._insert(
   File "/Users/alex/projects/django_constraint_issue/venv/lib/python3.10
 /site-packages/django/db/models/manager.py", line 87, in manager_method
     return getattr(self.get_queryset(), name)(*args, **kwargs)
   File "/Users/alex/projects/django_constraint_issue/venv/lib/python3.10
 /site-packages/django/db/models/query.py", line 1847, in _insert
     return query.get_compiler(using=using).execute_sql(returning_fields)
   File "/Users/alex/projects/django_constraint_issue/venv/lib/python3.10
 /site-packages/django/db/models/sql/compiler.py", line 1836, in
 execute_sql
     cursor.execute(sql, params)
   File "/Users/alex/projects/django_constraint_issue/venv/lib/python3.10
 /site-packages/django/db/backends/utils.py", line 122, in execute
     return super().execute(sql, params)
   File "/Users/alex/projects/django_constraint_issue/venv/lib/python3.10
 /site-packages/django/db/backends/utils.py", line 79, in execute
     return self._execute_with_wrappers(
   File "/Users/alex/projects/django_constraint_issue/venv/lib/python3.10
 /site-packages/django/db/backends/utils.py", line 92, in
 _execute_with_wrappers
     return executor(sql, params, many, context)
   File "/Users/alex/projects/django_constraint_issue/venv/lib/python3.10
 /site-packages/django/db/backends/utils.py", line 100, in _execute
     with self.db.wrap_database_errors:
   File "/Users/alex/projects/django_constraint_issue/venv/lib/python3.10
 /site-packages/django/db/utils.py", line 91, in __exit__
     raise dj_exc_value.with_traceback(traceback) from exc_value
   File "/Users/alex/projects/django_constraint_issue/venv/lib/python3.10
 /site-packages/django/db/backends/utils.py", line 105, in _execute
     return self.cursor.execute(sql, params)
 django.db.utils.InternalError: current transaction is aborted, commands
 ignored until end of transaction block
 }}}

 == Notes

 Whether you use an atomic transaction or not, Django _tries_ to validate
 the CheckConstraint before inserting the new Bar instance. This logs the
 following warning:
 {{{
 Got a database error calling check() on <Q: (AND:
 ExpressionWrapper(RawSQL(a = 1, [])))>: column "a" does not exist
 LINE 1: SELECT 1 AS "_check" WHERE COALESCE(((a = 1)), true)
 }}}
 Django appears to log this warning and then carry on. If the connection is
 in auto-commit mode, this is not a problem. The next statement will be
 executed in a new transaction. In an atomic block, this causes the
 `InternalError`.

 When you migrate, Django ''does'' warn about the CheckConstraint with
 RawSQL:

 {{{
 >>> python manage.py migrate
 System check identified some issues:

 WARNINGS:
 test_app.Bar: (models.W045) Check constraint 'a_is_1' contains RawSQL()
 expression and won't be validated during the model full_clean().
         HINT: Silence this warning if you don't care about it.
 }}}
 It seems that rather than skipping the validation of this constraint,
 Django is still validating it and then ignoring/logging the error. And it
 doesn't handle transaction state properly.
-- 
Ticket URL: <https://code.djangoproject.com/ticket/35712>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion on the web visit 
https://groups.google.com/d/msgid/django-updates/0107019194e8cd03-8b354f9c-26f2-4dd5-bd8f-ea58052d6986-000000%40eu-central-1.amazonses.com.

Reply via email to