Skip to content

Commit 41d8ef1

Browse files
charettessarahboyce
authored andcommitted
[5.0.x] Fixed #35625 -- Fixed a crash when adding a field with db_default and check constraint.
This is the exact same issue as refs #30408 but for creating a model with a constraint containing % escapes instead of column addition. All of these issues stem from a lack of SQL and parameters separation from the BaseConstraint DDL generating methods preventing them from being mixed with other parts of the schema alteration logic that do make use of parametrization on some backends (e.g. Postgres, MySQL for DEFAULT). Prior to the addition of Field.db_default and GeneratedField in 5.0 parametrization of DDL was never exercised on model creation so this is effectively a bug with db_default as the GeneratedField case was addressed by refs #35336. Thanks Julien Chaumont for the report and Mariusz Felisiak for the review. Backport of f359990 from main.
1 parent 68f6563 commit 41d8ef1

File tree

3 files changed

+83
-8
lines changed

3 files changed

+83
-8
lines changed

django/db/backends/base/schema.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ def __enter__(self):
164164
def __exit__(self, exc_type, exc_value, traceback):
165165
if exc_type is None:
166166
for sql in self.deferred_sql:
167-
self.execute(sql)
167+
self.execute(sql, None)
168168
if self.atomic_migration:
169169
self.atomic.__exit__(exc_type, exc_value, traceback)
170170

@@ -265,16 +265,29 @@ def table_sql(self, model):
265265
)
266266
if autoinc_sql:
267267
self.deferred_sql.extend(autoinc_sql)
268-
constraints = [
269-
constraint.constraint_sql(model, self)
270-
for constraint in model._meta.constraints
271-
]
268+
# The BaseConstraint DDL creation methods such as constraint_sql(),
269+
# create_sql(), and delete_sql(), were not designed in a way that
270+
# separate SQL from parameters which make their generated SQL unfit to
271+
# be used in a context where parametrization is delegated to the
272+
# backend.
273+
constraint_sqls = []
274+
if params:
275+
# If parameters are present (e.g. a DEFAULT clause on backends that
276+
# allow parametrization) defer constraint creation so they are not
277+
# mixed with SQL meant to be parametrized.
278+
for constraint in model._meta.constraints:
279+
self.deferred_sql.append(constraint.create_sql(model, self))
280+
else:
281+
constraint_sqls.extend(
282+
constraint.constraint_sql(model, self)
283+
for constraint in model._meta.constraints
284+
)
272285
sql = self.sql_create_table % {
273286
"table": self.quote_name(model._meta.db_table),
274287
"definition": ", ".join(
275-
str(constraint)
276-
for constraint in (*column_sqls, *constraints)
277-
if constraint
288+
str(statement)
289+
for statement in (*column_sqls, *constraint_sqls)
290+
if statement
278291
),
279292
}
280293
if model._meta.db_tablespace:

docs/releases/5.0.8.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,7 @@ Bugfixes
1515
* Fixed a regression in Django 5.0 where ``ModelAdmin.action_checkbox`` could
1616
break the admin changelist HTML page when rendering a model instance with a
1717
``__html__`` method (:ticket:`35606`).
18+
19+
* Fixed a crash when creating a model with a ``Field.db_default`` and a
20+
``Meta.constraints`` constraint composed of ``__endswith``, ``__startswith``,
21+
or ``__contains`` lookups (:ticket:`35625`).

tests/migrations/test_operations.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4086,6 +4086,64 @@ def test_add_constraint(self):
40864086
definition[2], {"model_name": "Pony", "constraint": gt_constraint}
40874087
)
40884088

4089+
@skipUnlessDBFeature("supports_table_check_constraints")
4090+
def test_create_model_constraint_percent_escaping(self):
4091+
app_label = "add_constraint_string_quoting"
4092+
from_state = ProjectState()
4093+
checks = [
4094+
# "%" generated in startswith lookup should be escaped in a way
4095+
# that is considered a leading wildcard.
4096+
(
4097+
models.Q(name__startswith="Albert"),
4098+
{"name": "Alberta"},
4099+
{"name": "Artur"},
4100+
),
4101+
# Literal "%" should be escaped in a way that is not a considered a
4102+
# wildcard.
4103+
(models.Q(rebate__endswith="%"), {"rebate": "10%"}, {"rebate": "10%$"}),
4104+
# Right-hand-side baked "%" literals should not be used for
4105+
# parameters interpolation.
4106+
(
4107+
~models.Q(surname__startswith=models.F("name")),
4108+
{"name": "Albert"},
4109+
{"name": "Albert", "surname": "Alberto"},
4110+
),
4111+
# Exact matches against "%" literals should also be supported.
4112+
(
4113+
models.Q(name="%"),
4114+
{"name": "%"},
4115+
{"name": "Albert"},
4116+
),
4117+
]
4118+
for check, valid, invalid in checks:
4119+
with self.subTest(condition=check, valid=valid, invalid=invalid):
4120+
constraint = models.CheckConstraint(condition=check, name="constraint")
4121+
operation = migrations.CreateModel(
4122+
"Author",
4123+
fields=[
4124+
("id", models.AutoField(primary_key=True)),
4125+
("name", models.CharField(max_length=100)),
4126+
("surname", models.CharField(max_length=100, db_default="")),
4127+
("rebate", models.CharField(max_length=100)),
4128+
],
4129+
options={"constraints": [constraint]},
4130+
)
4131+
to_state = from_state.clone()
4132+
operation.state_forwards(app_label, to_state)
4133+
with connection.schema_editor() as editor:
4134+
operation.database_forwards(app_label, editor, from_state, to_state)
4135+
Author = to_state.apps.get_model(app_label, "Author")
4136+
try:
4137+
with transaction.atomic():
4138+
Author.objects.create(**valid).delete()
4139+
with self.assertRaises(IntegrityError), transaction.atomic():
4140+
Author.objects.create(**invalid)
4141+
finally:
4142+
with connection.schema_editor() as editor:
4143+
migrations.DeleteModel("Author").database_forwards(
4144+
app_label, editor, to_state, from_state
4145+
)
4146+
40894147
@skipUnlessDBFeature("supports_table_check_constraints")
40904148
def test_add_constraint_percent_escaping(self):
40914149
app_label = "add_constraint_string_quoting"

0 commit comments

Comments
 (0)