#34949: Fails to create unique constraints
-------------------------------------+-------------------------------------
Reporter: Roman | Owner: nobody
Odaisky |
Type: Bug | Status: new
Component: Database | Version: dev
layer (models, ORM) |
Severity: Normal | Keywords:
Triage Stage: | Has patch: 0
Unreviewed |
Needs documentation: 0 | Needs tests: 0
Patch needs improvement: 0 | Easy pickings: 0
UI/UX: 0 |
-------------------------------------+-------------------------------------
Consider the following code:
{{{
class SomeModel(models.Model):
a = models.IntegerField()
b = models.IntegerField()
c = models.IntegerField()
d = models.IntegerField()
e = models.IntegerField()
f = models.IntegerField()
g = models.IntegerField(null=True)
class Meta:
indexes = [
models.Index(fields=["a"], name="index_a"),
models.Index(fields=["b"], include=["c"],
name="index_b_include_c"),
]
constraints = [
models.UniqueConstraint(fields=["d"], name="unique_d"),
models.UniqueConstraint(fields=["e"], include=["f"],
name="unique_e_include_f"),
models.UniqueConstraint(fields=["g"], nulls_distinct=True,
name="unique_g_nulls_distinct"),
]
}}}
= Expected Behavior =
From the documentation:
* `include` is ignored for databases besides PostgreSQL.
* `nulls_distinct` is ignored for databases besides PostgreSQL 15+.
One would imagine that if we run this on SQLite we’ll end up with five
indexes on one field each as though the unsupported parameters were not
present.
= Actual Behavior =
Instead we get:
* `index_a` on `a`
* `index_b_include_c` on `b`
* `unique_d` on `d`
* and **no** other constraints!
The documentation and the behavior are clearly at odds, and the behavior
is very confusing. The intention behind indexes like `unique_e_include_f`
is to enforce a constraint and to enable an optimization on top of that,
Django is unable to provide the optimization so it doesn’t enforce the
constraint either?
= Workflows Affected =
It seems to me there are two important workflows, none of which Django
currently supports:
1. A project for which the DB has been chosen long ago, and a migration to
a different one is very unlikely. The developer would like to get the most
out of the DB and is willing to use DB-specific features. In this case
Django should raise errors if the specific features requested are
unavailable.
2. A DB-agnostic app intended to be included in other projects. The
developer would like Django to create indexes that best match the
performance requirements as appropriate for whatever DB the app may end up
running on.
For example, in the second case, anticipating a lot of
`SomeModel.objects.filter(b__range=...).values("b", "c")` the developer
would like to create an `Index(fields=["b"], include=["c"])` but with a
fallback to `Index(fields=["b", "c"])` if covering indexes aren’t
supported. The interface to create custom indexes is
[https://docs.djangoproject.com/en/dev/ref/models/constraints/#baseconstraint
in theory documented] but very sparsely (it doesn’t even say what the
arguments to create_sql are) so expecting app developers to use this route
is unrealistic.
Note that while in many other cases failing to perform an optimization is
considered a graceful fallback, indexes are explicitly created for
purposes of optimization and failing to achieve that is an error that
should be signaled.
= Suggested Resolution =
1. Under no circumstances should Django quietly decline to create a unique
constraint.
2. Index and UniqueConstraint classes should have extra parameters to
specify what to fall back to if requested features are unavailable.
Perhaps something along the lines of
{{{
models.Index(
fields=["b"],
include=["c"],
fallback=models.Index(fields=["b", "c"]),
# where does the name go though?
)
models.UniqueConstraint(
fields=["b"],
include=["c"],
fallback=[
models.UniqueConstraint(fields=["b"]),
models.Index(fields=["b", "c"]),
],
)
models.UniqueConstraint(
fields=["x"],
nulls_distinct=True,
fallback=models.UniqueConstraint(Coalesce(F("x"), 0)),
)
}}}
3. There should be a setting to make it a warning or an error if Django
encounters an index that it can’t create exactly and that has no user-
specified fallback.
4. A perfect solution would also provide a way of upgrading indexes if a
later version of the DB adds support for the features that were previously
missing.
Here’s my attempt to approximate a solution:
{{{
class FallbackIndex(models.Index):
def __init__(self, *a, requires=None, fallback=None, **k):
super().__init__(*a, **k)
self.requires = requires
self.fallback = fallback
def create_sql(self, model, schema_editor, using="", **kwargs):
return (
super()
if getattr(schema_editor.connection.features, self.requires)
else self.fallback
).create_sql(model, schema_editor, using=using, **kwargs)
def deconstruct(self):
path, expressions, kwargs = super().deconstruct()
kwargs["requires"] = self.requires
kwargs["fallback"] = self.fallback
return path, expressions, kwargs
indexes = [
FallbackIndex(
fields=["b"],
include=["c"],
name="index_b_include_c",
requires="supports_covering_indexes",
fallback=models.Index(fields=["b", "c"],
name="index_b_include_c"),
),
]
}}}
except it should be possible to determine the required features
automatically.
--
Ticket URL: <https://code.djangoproject.com/ticket/34949>
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/0107018ba744663e-6ad4aed8-7e05-4029-a9f7-336a511575cd-000000%40eu-central-1.amazonses.com.