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