Opened 3 weeks ago
Last modified 3 weeks ago
#37351 assigned Bug
Altering the null attribute of a GeneratedField should raise when making migrations
| Reported by: | Jacob Walls | Owned by: | Md. Saikat Islam |
|---|---|---|---|
| Component: | Database layer (models, ORM) | Version: | 6.1 |
| Severity: | Normal | Keywords: | |
| Cc: | Natalia Bidart | Triage Stage: | Accepted |
| Has patch: | yes | Needs documentation: | no |
| Needs tests: | yes | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
The migration layer attempts to detect if a GeneratedField has been altered in a SQL-affecting way so that it can throw an exception:
if modifying_generated_field: raise ValueError( f"Modifying GeneratedFields is not supported - the field {new_field} " "must be removed and re-added with the new definition." )
However, altering the null kwarg, e.g. from False to True is not detected by the above check, but does have an incidence on the DDL that gets emitted, so a migration gets emitted that can do wacky things in the database, see ticket:37348#comment:6.
I propose to fix the modifying_generated_field logic so that it fathoms changes in the null attribute and raises.
This will be a breaking change but most likely a welcome one (removing dangerous operations from your migrations).
Rough test:
-
tests/migrations/test_operations.py
diff --git a/tests/migrations/test_operations.py b/tests/migrations/test_operations.py index 9bbf1249e9..907c255e22 100644
a b class OperationTests(OperationTestBase): 6646 6646 tests.append( 6647 6647 ("test_igfc_4", generated_1, generated_3), 6648 6648 ) 6649 generated_4 = models.GeneratedField( 6650 expression=F("pink") + F("pink") + F("pink"), 6651 output_field=models.IntegerField(), 6652 db_persist=db_persist, 6653 null=True, 6654 ) 6655 tests.append(("test_igfc_5", generated_2, generated_4)) 6649 6656 for app_label, add_field, alter_field in tests: 6650 6657 project_state = self.set_up_test_model(app_label) 6651 6658 operations = [
AssertionError: ValueError not raised
Change History (6)
comment:1 by , 3 weeks ago
comment:2 by , 3 weeks ago
| Triage Stage: | Unreviewed → Accepted |
|---|
Thanks Jacob. I agree the current behavior is a bug, so I'll Accept on that basis. But I have concerns about raising if modifying_generated_field holds:
- The proposed check is in the schema editor, therefore
makemigrationsstill generates theAlterFieldwhich is conceptually incorrect. - (Arguably worse) The error surfaces on
migrate, which is "too late": people who already have such anAlterFieldin their migration history would need to edit it, and a deploy is a bad moment to find that out. - Now that we merged the revert of W225 in #37348, users who removed
null=Trueto silence W225 need to add it back. Making a generated column nullable again is just dropping the NOT NULL constraint (ish), but under this proposal I think Django would refuse that and tell users to drop the column and add it again, which for a stored column means recomputing every row, and loses any untracked index or constraint. - This proposal is very backward incompatible, since existing
AlterFieldmigrations changingnullon generated fields have been applied successfully (when nullable or without NULL rows). Raising now would break migrating those projects (fully functional now) from scratch, including test databases.
So I think the root issue is that column creation and alteration disagree: _iter_column_sql() never emits NOT NULL for a generated column given this guards:
if field.generated: generated_sql, generated_params = self._column_generated_sql(field) params.extend(generated_params) yield generated_sql elif not null: yield "NOT NULL"
while _alter_field() does. That's also why the suggested workaround doesn't help, since removing and re-adding the field produces a nullable column no matter what null says. So the question to settle first is whether null should affect the DDL of a GeneratedField:
- If it shouldn't, the alteration should be skipped for generated fields which matches creation.
- If it should, creation needs to emit
NOT NULLas well.
comment:3 by , 3 weeks ago
I see! I wrongly assumed the existing check was invoked somehow during makemigrations, which would have solved 1, 2, and 4. Oops.
So the question to settle first is whether null should affect the DDL of a
GeneratedField
I guess we need to assess the cross-db feasibility of that and the backward compatibility implications before pursuing that?
comment:4 by , 3 weeks ago
I tested this locally by creating migrations and reproducing the issue.
class Pony(models.Model):
pink = models.IntegerField(default=3)
modified_pink = models.GeneratedField(
expression=F("pink") + F("pink"),
output_field=models.IntegerField(),
db_persist=True,
)
This results in the following SQLite schema:
CREATE TABLE IF NOT EXISTS "__main___pony" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"pink" integer NOT NULL,
"modified_pink" integer GENERATED ALWAYS AS (("pink" + "pink")) STORED
);
There is no NOT NULL constraint on modified_pink, regardless of the default null=False.
So, perhaps we should not consider null=True/null=False changes when generating migrations for GeneratedField. Since null=False currently has no effect on the generated column's DDL, ignoring changes to null could avoid generating an unnecessary AlterField.
I think this could also preserve backward compatibility with existing migrations that alter null on GeneratedField.
Does this approach make sense?
comment:5 by , 3 weeks ago
| Has patch: | set |
|---|---|
| Needs tests: | set |
| Owner: | set to |
| Status: | new → assigned |
comment:6 by , 3 weeks ago
Looking forward to hearing your thoughts on this PR: https://github.com/django/django/pull/21988
I’ve reproduced this issue and agree with the approach proposed in the ticket. I’ve also prepared a patch with a regression test for the
nullattribute change.I’d be happy to work on this ticket and raise a PR once the ticket is accepted.