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.
This commit is contained in:
Simon Charette 2024-07-23 00:33:31 -04:00 committed by Sarah Boyce
parent 8d6a20b656
commit f359990e49
3 changed files with 83 additions and 8 deletions

View file

@ -4107,6 +4107,64 @@ class OperationTests(OperationTestBase):
definition[2], {"model_name": "Pony", "constraint": gt_constraint}
)
@skipUnlessDBFeature("supports_table_check_constraints")
def test_create_model_constraint_percent_escaping(self):
app_label = "add_constraint_string_quoting"
from_state = ProjectState()
checks = [
# "%" generated in startswith lookup should be escaped in a way
# that is considered a leading wildcard.
(
models.Q(name__startswith="Albert"),
{"name": "Alberta"},
{"name": "Artur"},
),
# Literal "%" should be escaped in a way that is not a considered a
# wildcard.
(models.Q(rebate__endswith="%"), {"rebate": "10%"}, {"rebate": "10%$"}),
# Right-hand-side baked "%" literals should not be used for
# parameters interpolation.
(
~models.Q(surname__startswith=models.F("name")),
{"name": "Albert"},
{"name": "Albert", "surname": "Alberto"},
),
# Exact matches against "%" literals should also be supported.
(
models.Q(name="%"),
{"name": "%"},
{"name": "Albert"},
),
]
for check, valid, invalid in checks:
with self.subTest(condition=check, valid=valid, invalid=invalid):
constraint = models.CheckConstraint(condition=check, name="constraint")
operation = migrations.CreateModel(
"Author",
fields=[
("id", models.AutoField(primary_key=True)),
("name", models.CharField(max_length=100)),
("surname", models.CharField(max_length=100, db_default="")),
("rebate", models.CharField(max_length=100)),
],
options={"constraints": [constraint]},
)
to_state = from_state.clone()
operation.state_forwards(app_label, to_state)
with connection.schema_editor() as editor:
operation.database_forwards(app_label, editor, from_state, to_state)
Author = to_state.apps.get_model(app_label, "Author")
try:
with transaction.atomic():
Author.objects.create(**valid).delete()
with self.assertRaises(IntegrityError), transaction.atomic():
Author.objects.create(**invalid)
finally:
with connection.schema_editor() as editor:
migrations.DeleteModel("Author").database_forwards(
app_label, editor, to_state, from_state
)
@skipUnlessDBFeature("supports_table_check_constraints")
def test_add_constraint_percent_escaping(self):
app_label = "add_constraint_string_quoting"