#31197 closed Bug (fixed)
CheckConstraints with F objects don't apply on SQLite.
| Reported by: | Adam Johnson | Owned by: | nobody | 
|---|---|---|---|
| Component: | Database layer (models, ORM) | Version: | 2.2 | 
| Severity: | Normal | Keywords: | |
| Cc: | Simon Charette | Triage Stage: | Accepted | 
| Has patch: | no | Needs documentation: | no | 
| Needs tests: | no | Patch needs improvement: | no | 
| Easy pickings: | no | UI/UX: | no | 
Description
Take this model with CheckConstraint:
class Book(models.Model):
    percent_read = models.PositiveIntegerField()
    percent_unread = models.PositiveIntegerField()
    percent_ignored = models.PositiveIntegerField()
    def __str__(self):
        return f"{self.id} - {self.percent_read}% read"
    class Meta:
        constraints = [
            models.CheckConstraint(
                check=models.Q(
                    percent_read=(
                        100
                        - models.F("percent_unread")
                        - models.F("percent_ignored")
                    )
                ),
                name="percentages_sum_100",
            )
        ]
Applying the migration to create the constraint works on MariaDB but fails on SQLite.
The sqlmigrate output on SQLite shows the F objects get resolved to the full table name, which is what breaks during renaming phase in the migration:
$ python manage.py sqlmigrate core 0002
BEGIN;
--
-- Create constraint percentages_sum_100 on model book
--
CREATE TABLE "new__core_book" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "percent_read" integer unsigned NOT NULL CHECK ("percent_read" >= 0), "percent_unread" integer unsigned NOT NULL CHECK ("percent_unread" >= 0), "percent_ignored" integer unsigned NOT NULL CHECK ("percent_ignored" >= 0), CONSTRAINT "percentages_sum_100" CHECK ("percent_read" = ((100 - "new__core_book"."percent_unread") - "new__core_book"."percent_ignored")));
INSERT INTO "new__core_book" ("id", "percent_read", "percent_unread", "percent_ignored") SELECT "id", "percent_read", "percent_unread", "percent_ignored" FROM "core_book";
DROP TABLE "core_book";
ALTER TABLE "new__core_book" RENAME TO "core_book";
COMMIT;
$ python manage.py migrate
...
django.db.utils.DatabaseError: malformed database schema (core_book) - no such column: new__core_book.percent_unread
Full repo for reproduction: https://github.com/adamchainz/django-issue-check-constraint-reference
Change History (5)
comment:1 by , 6 years ago
comment:2 by , 6 years ago
| Triage Stage: | Unreviewed → Accepted | 
|---|---|
| Version: | master → 2.2 | 
This looks similar to #30754.
We don't pass simple_col anymore in master hence why I wasn't able to reproduce. It's an attribute of the Query (306b6875209cfedce2536a6679e69adee7c9bc6a).
this doesn't get passed down to the F objects because the intermediate CombinedExpressions don't pass on the parameter
There could a few things involved but CombinedExpressions effectively doesn't pass along simple_col, in fact it doesn't even accepts it.
comment:3 by , 6 years ago
| Resolution: | → fixed | 
|---|---|
| Status: | new → closed | 
| Summary: | CheckConstraints with F objects don't apply on SQLite → CheckConstraints with F objects don't apply on SQLite. | 
This is already fixed on master. Django 2.2 is in extended support and it's not a regression in Django 3.0 so fix doesn't qualify for a backport. I'm going to add a test case to avoid regressions in the future.
Fixed in 306b6875209cfedce2536a6679e69adee7c9bc6a.
comment:5 by , 6 years ago
Thanks both. I checked only with stable/3.0.x branch and forgot to see if fixed in master.
For anyone finding this issue, the workaround would be to use SeparateDatabaseAndState with the autodetected operations in the state_operations and a RunSQL in the database_operations that executes the fixed SQL.
If I apply this patch, it works:
diff --git django/db/models/expressions.py django/db/models/expressions.py index 2b59dd301a..5ee3baf6f1 100644 --- django/db/models/expressions.py +++ django/db/models/expressions.py @@ -528,6 +528,8 @@ class F(Combinable): def resolve_expression(self, query=None, allow_joins=True, reuse=None, summarize=False, for_save=False, simple_col=False): + # Override + simple_col = True return query.resolve_ref(self.name, allow_joins, reuse, summarize, simple_col) def asc(self, **kwargs):It looks like
CheckConstraint._get_check_sqlends up passingsimple_col=TrueinQuery.build_filterbut this doesn't get passed down to theFobjects because the intermediateCombinedExpressions don't pass on the parameter?