diff --git a/CHANGES.md b/CHANGES.md index 4d15e60..855f8f0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,7 +4,9 @@ - changed `ADD COLUMN DEFAULT NULL` to safe operation for code default - changed `ADD COLUMN DEFAULT NOT NULL` to safe operation for `db_default` in django 5.0+ - added `ZERO_DOWNTIME_MIGRATIONS_KEEP_DEFAULT` settings and changed `ADD COLUMN DEFAULT NOT NULL` with this settings to safe operation for django<5.0 + - added `ZERO_DOWNTIME_MIGRATIONS_EXPLICIT_CONSTRAINTS_DROP` settings and added dropping constraints and indexes before drop column or table - fixed sqlmigrate in idempotent mode + - fixed creation unique constraint with include parameter - updated unsafe migrations links to documentation - updated patched code to latest django version - updated test image to ubuntu 24.04 diff --git a/README.md b/README.md index e75385b..1806b78 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,18 @@ To avoid manual schema manipulation idempotent mode allows to rerun failed migra > _NOTE:_ idempotent mode checks rely only on name and index and constraint valid state, so it can ignore name collisions and recommended do not use it for CI checks. +#### ZERO_DOWNTIME_MIGRATIONS_EXPLICIT_CONSTRAINTS_DROP + +Define way to drop foreign key, unique constraints and indexes before drop table or column, default `True`: + + ZERO_DOWNTIME_MIGRATIONS_EXPLICIT_CONSTRAINTS_DROP = True + +Allowed values: +- `True` - before dropping table drop all foreign keys related to this table and before dropping column drop all foreign keys related to this column, unique constraints on this column and indexes used this column. +- `False` - standard django behaviour that will drop constraints with `CASCADE` mode (some constraints can be dropped explicitly too). + +Explicitly dropping constraints and indexes before dropping tables or columns allows for splitting schema-only changes with an `ACCESS EXCLUSIVE` lock and the deletion of physical files, which can take significant time and cause downtime. + #### ZERO_DOWNTIME_MIGRATIONS_KEEP_DEFAULT Define way keep or drop code defaults on database level when adding new column, default `False`: @@ -234,7 +246,7 @@ Postgres store values of different types different ways. If you try to convert o ### Multiversion Concurrency Control -Regarding documentation https://www.postgresql.org/docs/current/static/mvcc-intro.html data consistency in postgres is maintained by using a multiversion model. This means that each SQL statement sees a snapshot of data. It has advantage for adding and deleting columns without any indexes, constrains and defaults do not change exist data, new version of data will be created on `INSERT` and `UPDATE`, delete just mark you record expired. All garbage will be collected later by `VACUUM` or `AUTO VACUUM`. +Regarding documentation https://www.postgresql.org/docs/current/static/mvcc-intro.html data consistency in postgres is maintained by using a multiversion model. This means that each SQL statement sees a snapshot of data. It has advantage for adding and deleting columns without any indexes, CONSTRAINTS and defaults do not change exist data, new version of data will be created on `INSERT` and `UPDATE`, delete just mark you record expired. All garbage will be collected later by `VACUUM` or `AUTO VACUUM`. ### Django migrations hacks diff --git a/django_zero_downtime_migrations/backends/postgres/schema.py b/django_zero_downtime_migrations/backends/postgres/schema.py index 71ff5e7..c1f0c20 100644 --- a/django_zero_downtime_migrations/backends/postgres/schema.py +++ b/django_zero_downtime_migrations/backends/postgres/schema.py @@ -5,7 +5,7 @@ import django from django.conf import settings from django.contrib.postgres.constraints import ExclusionConstraint -from django.db.backends.ddl_references import Statement +from django.db.backends.ddl_references import Statement, Table from django.db.backends.postgresql.schema import ( DatabaseSchemaEditor as PostgresDatabaseSchemaEditor ) @@ -417,7 +417,8 @@ class DatabaseSchemaEditorMixin: if django.VERSION[:2] >= (5, 0): sql_create_unique_index = MultiStatementSQL( PGShareUpdateExclusive( - "CREATE UNIQUE INDEX CONCURRENTLY %(name)s ON %(table)s (%(columns)s)%(condition)s%(nulls_distinct)s", + "CREATE UNIQUE INDEX CONCURRENTLY %(name)s ON %(table)s " + "(%(columns)s)%(include)s%(nulls_distinct)s%(condition)s", idempotent_condition=Condition(_sql_index_exists, False), disable_statement_timeout=True, ), @@ -430,7 +431,8 @@ class DatabaseSchemaEditorMixin: else: sql_create_unique_index = MultiStatementSQL( PGShareUpdateExclusive( - "CREATE UNIQUE INDEX CONCURRENTLY %(name)s ON %(table)s (%(columns)s)%(condition)s", + "CREATE UNIQUE INDEX CONCURRENTLY %(name)s ON %(table)s " + "(%(columns)s)%(include)s%(condition)s", idempotent_condition=Condition(_sql_index_exists, False), disable_statement_timeout=True, ), @@ -465,6 +467,54 @@ class DatabaseSchemaEditorMixin: ), ) + _sql_get_table_constraints_introspection = r""" + SELECT + c.conname, + c.contype, + c.conrelid::regclass::text, + c.confrelid::regclass::text, + array( + SELECT attname + FROM unnest(c.conkey) WITH ORDINALITY cols(colid, arridx) + JOIN pg_attribute AS ca ON cols.colid = ca.attnum + WHERE ca.attrelid = c.conrelid + ORDER BY cols.arridx + ), + array( + SELECT attname + FROM unnest(c.confkey) WITH ORDINALITY cols(colid, arridx) + JOIN pg_attribute AS ca ON cols.colid = ca.attnum + WHERE ca.attrelid = c.confrelid + ORDER BY cols.arridx + ) + FROM pg_constraint AS c + WHERE c.conrelid::regclass::text = %s + OR c.confrelid::regclass::text = %s + ORDER BY c.conrelid::regclass::text, c.conname + """ + _sql_get_index_introspection = r""" + SELECT + i.indexrelid::regclass::text, + i.indrelid::regclass::text, + array( + SELECT a.attname + FROM ( + SELECT unnest(i.indkey) + UNION + SELECT unnest(regexp_matches(i.indexprs::text, ':varattno (\d+)', 'g'))::int + UNION + SELECT unnest(regexp_matches(i.indpred::text, ':varattno (\d+)', 'g'))::int + ) cols(varattno) + INNER JOIN pg_attribute AS a ON cols.varattno = a.attnum + WHERE a.attrelid = i.indrelid + ) + FROM pg_index i + LEFT JOIN pg_constraint c ON i.indexrelid = c.conindid + WHERE indrelid::regclass::text = %s + AND c.conindid IS NULL + ORDER BY i.indrelid::regclass::text, i.indexrelid::regclass::text + """ + _varchar_type_regexp = re.compile(r'^varchar\((?P\d+)\)$') _numeric_type_regexp = re.compile(r'^numeric\((?P\d+), *(?P\d+)\)$') @@ -508,6 +558,7 @@ def __init__(self, connection, collect_sql=False, atomic=True): DeprecationWarning, ) self.KEEP_DEFAULT = False + self.EXPLICIT_CONSTRAINTS_DROP = getattr(settings, "ZERO_DOWNTIME_MIGRATIONS_EXPLICIT_CONSTRAINTS_DROP", True) def execute(self, sql, params=()): if sql is DUMMY_SQL: @@ -599,13 +650,58 @@ def _flush_deferred_sql(self): self.execute(sql) self.deferred_sql.clear() + def _get_constraints(self, cursor, model): + cursor.execute(self._sql_get_table_constraints_introspection, [model._meta.db_table, model._meta.db_table]) + for constraint, kind, table, table_ref, columns, columns_ref in cursor.fetchall(): + yield constraint, kind, table, table_ref, columns, columns_ref + + def _get_indexes(self, cursor, model): + cursor.execute(self._sql_get_index_introspection, [model._meta.db_table]) + for index, table, columns in cursor.fetchall(): + yield index, table, columns + + def _drop_collect_sql_introspection_related_duplicates(self, drop_constraint_queries): + """ + django internals use introspection to find related constraints and perform action if constraint exists + dropping constraints before dropping table or column can duplicate same logic in django internals + in this case for sqlmigrate drop constraint sql can be duplicated as no physical constraint drop perform + so just remove constraint drop duplicates for sqlmigrate + """ + if self.collect_sql: + handled_queries = set() + drops = set() + for i in range(len(self.collected_sql)): + for j in range(len(drop_constraint_queries)): + if all( + self.collected_sql[i + k] == drop_constraint_queries[j][k] + for k in range(len(drop_constraint_queries[j])) + ): + if j in handled_queries: + drops |= {i + k for k in range(len(drop_constraint_queries[j]))} + handled_queries.add(j) + self.collected_sql = [query for i, query in enumerate(self.collected_sql) if i not in drops] + def create_model(self, model): super().create_model(model) self._flush_deferred_sql() def delete_model(self, model): + drop_constraint_queries = [] + if self.EXPLICIT_CONSTRAINTS_DROP: + with self.connection.cursor() as cursor: + for constraint, kind, table, table_ref, columns, columns_ref in self._get_constraints(cursor, model): + if kind == "f": + last_collected_sql = len(self.collected_sql) if self.collect_sql else None + self.execute(Statement( + self.sql_delete_fk, + table=Table(table, self.quote_name), + name=self.quote_name(constraint), + )) + if self.collect_sql: + drop_constraint_queries.append(self.collected_sql[last_collected_sql:]) super().delete_model(model) self._flush_deferred_sql() + self._drop_collect_sql_introspection_related_duplicates(drop_constraint_queries) def alter_index_together(self, model, old_index_together, new_index_together): super().alter_index_together(model, old_index_together, new_index_together) @@ -641,8 +737,56 @@ def add_field(self, model, field): self._flush_deferred_sql() def remove_field(self, model, field): + drop_constraint_queries = [] + if self.EXPLICIT_CONSTRAINTS_DROP: + with self.connection.cursor() as cursor: + # as foreign key can have index as dependent object it important to drop all foreign keys first + for constraint, kind, table, table_ref, columns, columns_ref in self._get_constraints(cursor, model): + # drop foreign key for current model columns + if kind == "f" and table == model._meta.db_table and field.column in columns: + last_collected_sql = len(self.collected_sql) if self.collect_sql else None + self.execute(Statement( + self.sql_delete_fk, + table=Table(table, self.quote_name), + name=self.quote_name(constraint), + )) + if self.collect_sql: + drop_constraint_queries.append(self.collected_sql[last_collected_sql:]) + # drop foreign key for target model columns, i.e. backrefs + if kind == "f" and table_ref == model._meta.db_table and field.column in columns_ref: + last_collected_sql = len(self.collected_sql) if self.collect_sql else None + self.execute(Statement( + self.sql_delete_fk, + table=Table(table, self.quote_name), + name=self.quote_name(constraint), + )) + if self.collect_sql: + drop_constraint_queries.append(self.collected_sql[last_collected_sql:]) + for constraint, kind, table, table_ref, columns, columns_ref in self._get_constraints(cursor, model): + # drop unique constraints for current model columns + if kind == "u" and table == model._meta.db_table and field.column in columns: + last_collected_sql = len(self.collected_sql) if self.collect_sql else None + self.execute(Statement( + self.sql_delete_unique, + table=Table(table, self.quote_name), + name=self.quote_name(constraint), + )) + if self.collect_sql: + drop_constraint_queries.append(self.collected_sql[last_collected_sql:]) + for index, table, columns in self._get_indexes(cursor, model): + # drop indexes for current model columns + if table == model._meta.db_table and field.column in columns: + last_collected_sql = len(self.collected_sql) if self.collect_sql else None + self.execute(Statement( + self.sql_delete_index_concurrently, + table=Table(table, self.quote_name), + name=self.quote_name(index), + )) + if self.collect_sql: + drop_constraint_queries.append(self.collected_sql[last_collected_sql:]) super().remove_field(model, field) self._flush_deferred_sql() + self._drop_collect_sql_introspection_related_duplicates(drop_constraint_queries) def alter_field(self, model, old_field, new_field, strict=False): super().alter_field(model, old_field, new_field, strict) diff --git a/tests/apps/good_flow_drop_column_with_constraints/__init__.py b/tests/apps/good_flow_drop_column_with_constraints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/apps/good_flow_drop_column_with_constraints/migrations/0001_initial.py b/tests/apps/good_flow_drop_column_with_constraints/migrations/0001_initial.py new file mode 100644 index 0000000..106a387 --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints/migrations/0001_initial.py @@ -0,0 +1,187 @@ +import django.db.models.deletion +import django.db.models.functions.math +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="TestTableMain", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("main_id", models.IntegerField(null=True, unique=True)), + ("field_u1", models.IntegerField(null=True)), + ("field_u2", models.IntegerField(null=True)), + ("field_u3", models.IntegerField(null=True)), + ("field_u4", models.IntegerField(null=True)), + ("field_u5", models.IntegerField(null=True)), + ("field_u6", models.IntegerField(null=True)), + ("field_u7", models.IntegerField(null=True)), + ("field_i1", models.IntegerField(null=True)), + ("field_i2", models.IntegerField(null=True)), + ("field_i3", models.IntegerField(null=True)), + ("field_i4", models.IntegerField(null=True)), + ("field_i5", models.IntegerField(null=True)), + ("field_i6", models.IntegerField(null=True)), + ("field_i7", models.IntegerField(null=True)), + ], + options={ + "db_table": "drop_col_test_table_main", + }, + ), + migrations.CreateModel( + name="TestTableParent", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ], + options={ + "db_table": "drop_col_test_table_parent", + }, + ), + migrations.CreateModel( + name="TestTableChild", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "main", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="good_flow_drop_column_with_constraints.testtablemain", + to_field="main_id", + ), + ), + ], + options={ + "db_table": "drop_col_test_table_child", + }, + ), + migrations.AddField( + model_name="testtablemain", + name="parent", + field=models.OneToOneField( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="good_flow_drop_column_with_constraints.testtableparent", + ), + ), + migrations.AddIndex( + model_name="testtablemain", + index=models.Index( + models.F("parent"), models.F("field_i1"), name="drop_col_i1" + ), + ), + migrations.AddIndex( + model_name="testtablemain", + index=models.Index(fields=["parent", "field_i2"], name="drop_col_i2"), + ), + migrations.AddIndex( + model_name="testtablemain", + index=models.Index( + django.db.models.functions.math.Abs("field_i3"), name="drop_col_i3" + ), + ), + migrations.AddIndex( + model_name="testtablemain", + index=models.Index( + models.F("parent"), + condition=models.Q(("field_i4__gt", 0)), + name="drop_col_i4", + ), + ), + migrations.AddIndex( + model_name="testtablemain", + index=models.Index( + condition=models.Q(("field_i5__gt", 0)), + fields=["parent"], + name="drop_col_i5", + ), + ), + migrations.AddIndex( + model_name="testtablemain", + index=models.Index( + models.F("parent"), include=("field_i6",), name="drop_col_i6" + ), + ), + migrations.AddIndex( + model_name="testtablemain", + index=models.Index( + fields=["parent"], include=("field_i7",), name="drop_col_i7" + ), + ), + migrations.AddConstraint( + model_name="testtablemain", + constraint=models.UniqueConstraint( + models.F("parent"), models.F("field_u1"), name="drop_col_u1" + ), + ), + migrations.AddConstraint( + model_name="testtablemain", + constraint=models.UniqueConstraint( + fields=("parent", "field_u2"), name="drop_col_u2" + ), + ), + migrations.AddConstraint( + model_name="testtablemain", + constraint=models.UniqueConstraint( + django.db.models.functions.math.Abs("field_u3"), name="drop_col_u3" + ), + ), + migrations.AddConstraint( + model_name="testtablemain", + constraint=models.UniqueConstraint( + models.F("parent"), + condition=models.Q(("field_u4__gt", 0)), + name="drop_col_u4", + ), + ), + migrations.AddConstraint( + model_name="testtablemain", + constraint=models.UniqueConstraint( + condition=models.Q(("field_u5__gt", 0)), + fields=("parent",), + name="drop_col_u5", + ), + ), + migrations.AddConstraint( + model_name="testtablemain", + constraint=models.UniqueConstraint( + models.F("parent"), include=("field_u6",), name="drop_col_u6" + ), + ), + migrations.AddConstraint( + model_name="testtablemain", + constraint=models.UniqueConstraint( + fields=("parent",), include=("field_u7",), name="drop_col_u7" + ), + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints/migrations/0002_remove_testtablemain_drop_col_u1_and_more.py b/tests/apps/good_flow_drop_column_with_constraints/migrations/0002_remove_testtablemain_drop_col_u1_and_more.py new file mode 100644 index 0000000..6da64cf --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints/migrations/0002_remove_testtablemain_drop_col_u1_and_more.py @@ -0,0 +1,87 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("good_flow_drop_column_with_constraints", "0001_initial"), + ] + + operations = [ + # emulate worst case untracked constraints + migrations.SeparateDatabaseAndState( + database_operations=[ + # as constraint dropped with cascade or explicitly before cascade + # we need to back untracked constraint creation to make migration revert happy + migrations.RunSQL( + migrations.RunSQL.noop, + """ + ALTER TABLE "drop_col_test_table_main" + ADD CONSTRAINT "drop_col_u2" UNIQUE ("parent_id", "field_u2"); + """ + ) + ], + state_operations=[ + migrations.RemoveConstraint( + model_name="testtablemain", + name="drop_col_u1", + ), + migrations.RemoveConstraint( + model_name="testtablemain", + name="drop_col_u2", + ), + migrations.RemoveConstraint( + model_name="testtablemain", + name="drop_col_u3", + ), + migrations.RemoveConstraint( + model_name="testtablemain", + name="drop_col_u4", + ), + migrations.RemoveConstraint( + model_name="testtablemain", + name="drop_col_u5", + ), + migrations.RemoveConstraint( + model_name="testtablemain", + name="drop_col_u6", + ), + migrations.RemoveConstraint( + model_name="testtablemain", + name="drop_col_u7", + ), + migrations.RemoveIndex( + model_name="testtablemain", + name="drop_col_i1", + ), + migrations.RemoveIndex( + model_name="testtablemain", + name="drop_col_i2", + ), + migrations.RemoveIndex( + model_name="testtablemain", + name="drop_col_i3", + ), + migrations.RemoveIndex( + model_name="testtablemain", + name="drop_col_i4", + ), + migrations.RemoveIndex( + model_name="testtablemain", + name="drop_col_i5", + ), + migrations.RemoveIndex( + model_name="testtablemain", + name="drop_col_i6", + ), + migrations.RemoveIndex( + model_name="testtablemain", + name="drop_col_i7", + ), + migrations.RemoveField( + model_name="testtablechild", + name="main", + ), + ], + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints/migrations/0003_remove_testtablemain_field_i7.py b/tests/apps/good_flow_drop_column_with_constraints/migrations/0003_remove_testtablemain_field_i7.py new file mode 100644 index 0000000..54c7e73 --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints/migrations/0003_remove_testtablemain_field_i7.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ( + "good_flow_drop_column_with_constraints", + "0002_remove_testtablemain_drop_col_u1_and_more", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="field_i7", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints/migrations/0004_remove_testtablemain_field_i6.py b/tests/apps/good_flow_drop_column_with_constraints/migrations/0004_remove_testtablemain_field_i6.py new file mode 100644 index 0000000..632743d --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints/migrations/0004_remove_testtablemain_field_i6.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ( + "good_flow_drop_column_with_constraints", + "0003_remove_testtablemain_field_i7", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="field_i6", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints/migrations/0005_remove_testtablemain_field_i5.py b/tests/apps/good_flow_drop_column_with_constraints/migrations/0005_remove_testtablemain_field_i5.py new file mode 100644 index 0000000..6d0cdd4 --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints/migrations/0005_remove_testtablemain_field_i5.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ( + "good_flow_drop_column_with_constraints", + "0004_remove_testtablemain_field_i6", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="field_i5", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints/migrations/0006_remove_testtablemain_field_i4.py b/tests/apps/good_flow_drop_column_with_constraints/migrations/0006_remove_testtablemain_field_i4.py new file mode 100644 index 0000000..bb91e80 --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints/migrations/0006_remove_testtablemain_field_i4.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ( + "good_flow_drop_column_with_constraints", + "0005_remove_testtablemain_field_i5", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="field_i4", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints/migrations/0007_remove_testtablemain_field_i3.py b/tests/apps/good_flow_drop_column_with_constraints/migrations/0007_remove_testtablemain_field_i3.py new file mode 100644 index 0000000..398e85a --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints/migrations/0007_remove_testtablemain_field_i3.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ( + "good_flow_drop_column_with_constraints", + "0006_remove_testtablemain_field_i4", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="field_i3", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints/migrations/0008_remove_testtablemain_field_i2.py b/tests/apps/good_flow_drop_column_with_constraints/migrations/0008_remove_testtablemain_field_i2.py new file mode 100644 index 0000000..deb9a17 --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints/migrations/0008_remove_testtablemain_field_i2.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ( + "good_flow_drop_column_with_constraints", + "0007_remove_testtablemain_field_i3", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="field_i2", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints/migrations/0009_remove_testtablemain_field_i1.py b/tests/apps/good_flow_drop_column_with_constraints/migrations/0009_remove_testtablemain_field_i1.py new file mode 100644 index 0000000..2e6715c --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints/migrations/0009_remove_testtablemain_field_i1.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ( + "good_flow_drop_column_with_constraints", + "0008_remove_testtablemain_field_i2", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="field_i1", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints/migrations/0010_remove_testtablemain_field_u7.py b/tests/apps/good_flow_drop_column_with_constraints/migrations/0010_remove_testtablemain_field_u7.py new file mode 100644 index 0000000..d06c5e4 --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints/migrations/0010_remove_testtablemain_field_u7.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ( + "good_flow_drop_column_with_constraints", + "0009_remove_testtablemain_field_i1", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="field_u7", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints/migrations/0011_remove_testtablemain_field_u6.py b/tests/apps/good_flow_drop_column_with_constraints/migrations/0011_remove_testtablemain_field_u6.py new file mode 100644 index 0000000..7a8d36f --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints/migrations/0011_remove_testtablemain_field_u6.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ( + "good_flow_drop_column_with_constraints", + "0010_remove_testtablemain_field_u7", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="field_u6", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints/migrations/0012_remove_testtablemain_field_u5.py b/tests/apps/good_flow_drop_column_with_constraints/migrations/0012_remove_testtablemain_field_u5.py new file mode 100644 index 0000000..63b3bb6 --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints/migrations/0012_remove_testtablemain_field_u5.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ( + "good_flow_drop_column_with_constraints", + "0011_remove_testtablemain_field_u6", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="field_u5", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints/migrations/0013_remove_testtablemain_field_u4.py b/tests/apps/good_flow_drop_column_with_constraints/migrations/0013_remove_testtablemain_field_u4.py new file mode 100644 index 0000000..4465705 --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints/migrations/0013_remove_testtablemain_field_u4.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ( + "good_flow_drop_column_with_constraints", + "0012_remove_testtablemain_field_u5", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="field_u4", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints/migrations/0014_remove_testtablemain_field_u3.py b/tests/apps/good_flow_drop_column_with_constraints/migrations/0014_remove_testtablemain_field_u3.py new file mode 100644 index 0000000..7c98fa1 --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints/migrations/0014_remove_testtablemain_field_u3.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ( + "good_flow_drop_column_with_constraints", + "0013_remove_testtablemain_field_u4", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="field_u3", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints/migrations/0015_remove_testtablemain_field_u2.py b/tests/apps/good_flow_drop_column_with_constraints/migrations/0015_remove_testtablemain_field_u2.py new file mode 100644 index 0000000..49baba3 --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints/migrations/0015_remove_testtablemain_field_u2.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ( + "good_flow_drop_column_with_constraints", + "0014_remove_testtablemain_field_u3", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="field_u2", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints/migrations/0016_remove_testtablemain_field_u1.py b/tests/apps/good_flow_drop_column_with_constraints/migrations/0016_remove_testtablemain_field_u1.py new file mode 100644 index 0000000..cc2229b --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints/migrations/0016_remove_testtablemain_field_u1.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ( + "good_flow_drop_column_with_constraints", + "0015_remove_testtablemain_field_u2", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="field_u1", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints/migrations/0017_remove_testtablemain_main_id.py b/tests/apps/good_flow_drop_column_with_constraints/migrations/0017_remove_testtablemain_main_id.py new file mode 100644 index 0000000..77316ee --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints/migrations/0017_remove_testtablemain_main_id.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ( + "good_flow_drop_column_with_constraints", + "0016_remove_testtablemain_field_u1", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="main_id", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints/migrations/0018_remove_testtablemain_parent.py b/tests/apps/good_flow_drop_column_with_constraints/migrations/0018_remove_testtablemain_parent.py new file mode 100644 index 0000000..d4b99f6 --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints/migrations/0018_remove_testtablemain_parent.py @@ -0,0 +1,17 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ("good_flow_drop_column_with_constraints", "0017_remove_testtablemain_main_id"), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="parent", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints/migrations/__init__.py b/tests/apps/good_flow_drop_column_with_constraints/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/apps/good_flow_drop_column_with_constraints/models.py b/tests/apps/good_flow_drop_column_with_constraints/models.py new file mode 100644 index 0000000..9355d16 --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints/models.py @@ -0,0 +1,56 @@ +from django.db import models + +# from django.db.models.functions import Abs + + +class TestTableParent(models.Model): + + class Meta: + db_table = 'drop_col_test_table_parent' + + +class TestTableMain(models.Model): + # parent = models.OneToOneField(TestTableParent, null=True, on_delete=models.CASCADE) + # main_id = models.IntegerField(null=True, unique=True) + # field_u1 = models.IntegerField(null=True) + # field_u2 = models.IntegerField(null=True) + # field_u3 = models.IntegerField(null=True) + # field_u4 = models.IntegerField(null=True) + # field_u5 = models.IntegerField(null=True) + # field_u6 = models.IntegerField(null=True) + # field_u7 = models.IntegerField(null=True) + # field_i1 = models.IntegerField(null=True) + # field_i2 = models.IntegerField(null=True) + # field_i3 = models.IntegerField(null=True) + # field_i4 = models.IntegerField(null=True) + # field_i5 = models.IntegerField(null=True) + # field_i6 = models.IntegerField(null=True) + # field_i7 = models.IntegerField(null=True) + + class Meta: + db_table = 'drop_col_test_table_main' + # constraints = [ + # models.UniqueConstraint("parent", "field_u1", name="drop_col_u1"), + # models.UniqueConstraint(fields=["parent", "field_u2"], name="drop_col_u2"), + # models.UniqueConstraint(Abs("field_u3"), name="drop_col_u3"), + # models.UniqueConstraint("parent", name="drop_col_u4", condition=models.Q(field_u4__gt=0)), + # models.UniqueConstraint(fields=["parent"], name="drop_col_u5", condition=models.Q(field_u5__gt=0)), + # models.UniqueConstraint("parent", name="drop_col_u6", include=["field_u6"]), + # models.UniqueConstraint(fields=["parent"], name="drop_col_u7", include=["field_u7"]), + # ] + # indexes = [ + # models.Index("parent", "field_i1", name="drop_col_i1"), + # models.Index(fields=["parent", "field_i2"], name="drop_col_i2"), + # models.Index(Abs("field_i3"), name="drop_col_i3"), + # models.Index("parent", name="drop_col_i4", condition=models.Q(field_i4__gt=0)), + # models.Index(fields=["parent"], name="drop_col_i5", condition=models.Q(field_i5__gt=0)), + # models.Index("parent", name="drop_col_i6", include=["field_i6"]), + # models.Index(fields=["parent"], name="drop_col_i7", include=["field_i7"]), + # ] + + +class TestTableChild(models.Model): + # main = models.ForeignKey(TestTableMain, to_field="main_id", null=True, on_delete=models.CASCADE) + + class Meta: + db_table = 'drop_col_test_table_child' diff --git a/tests/apps/good_flow_drop_column_with_constraints_old/__init__.py b/tests/apps/good_flow_drop_column_with_constraints_old/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0001_initial.py b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0001_initial.py new file mode 100644 index 0000000..052e72d --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0001_initial.py @@ -0,0 +1,157 @@ +import django.db.models.deletion +import django.db.models.functions.math +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="TestTableMain", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("main_id", models.IntegerField(null=True, unique=True)), + ("field_u2", models.IntegerField(null=True)), + ("field_u5", models.IntegerField(null=True)), + ("field_u7", models.IntegerField(null=True)), + ("field_i1", models.IntegerField(null=True)), + ("field_i2", models.IntegerField(null=True)), + ("field_i3", models.IntegerField(null=True)), + ("field_i4", models.IntegerField(null=True)), + ("field_i5", models.IntegerField(null=True)), + ("field_i6", models.IntegerField(null=True)), + ("field_i7", models.IntegerField(null=True)), + ], + options={ + "db_table": "drop_col_test_table_main", + }, + ), + migrations.CreateModel( + name="TestTableParent", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ], + options={ + "db_table": "drop_col_test_table_parent", + }, + ), + migrations.CreateModel( + name="TestTableChild", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "main", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="good_flow_drop_column_with_constraints_old.testtablemain", + to_field="main_id", + ), + ), + ], + options={ + "db_table": "drop_col_test_table_child", + }, + ), + migrations.AddField( + model_name="testtablemain", + name="parent", + field=models.OneToOneField( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="good_flow_drop_column_with_constraints_old.testtableparent", + ), + ), + migrations.AddIndex( + model_name="testtablemain", + index=models.Index( + models.F("parent"), models.F("field_i1"), name="drop_col_i1" + ), + ), + migrations.AddIndex( + model_name="testtablemain", + index=models.Index(fields=["parent", "field_i2"], name="drop_col_i2"), + ), + migrations.AddIndex( + model_name="testtablemain", + index=models.Index( + django.db.models.functions.math.Abs("field_i3"), name="drop_col_i3" + ), + ), + migrations.AddIndex( + model_name="testtablemain", + index=models.Index( + models.F("parent"), + condition=models.Q(("field_i4__gt", 0)), + name="drop_col_i4", + ), + ), + migrations.AddIndex( + model_name="testtablemain", + index=models.Index( + condition=models.Q(("field_i5__gt", 0)), + fields=["parent"], + name="drop_col_i5", + ), + ), + migrations.AddIndex( + model_name="testtablemain", + index=models.Index( + models.F("parent"), include=("field_i6",), name="drop_col_i6" + ), + ), + migrations.AddIndex( + model_name="testtablemain", + index=models.Index( + fields=["parent"], include=("field_i7",), name="drop_col_i7" + ), + ), + migrations.AddConstraint( + model_name="testtablemain", + constraint=models.UniqueConstraint( + fields=("parent", "field_u2"), name="drop_col_u2" + ), + ), + migrations.AddConstraint( + model_name="testtablemain", + constraint=models.UniqueConstraint( + condition=models.Q(("field_u5__gt", 0)), + fields=("parent",), + name="drop_col_u5", + ), + ), + migrations.AddConstraint( + model_name="testtablemain", + constraint=models.UniqueConstraint( + fields=("parent",), include=("field_u7",), name="drop_col_u7" + ), + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0002_remove_testtablemain_drop_col_u2_and_more.py b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0002_remove_testtablemain_drop_col_u2_and_more.py new file mode 100644 index 0000000..53b29b9 --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0002_remove_testtablemain_drop_col_u2_and_more.py @@ -0,0 +1,71 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("good_flow_drop_column_with_constraints_old", "0001_initial"), + ] + + operations = [ + # emulate worst case untracked constraints + migrations.SeparateDatabaseAndState( + database_operations=[ + # as constraint dropped with cascade or explicitly before cascade + # we need to back untracked constraint creation to make migration revert happy + migrations.RunSQL( + migrations.RunSQL.noop, + """ + ALTER TABLE "drop_col_test_table_main" + ADD CONSTRAINT "drop_col_u2" UNIQUE ("parent_id", "field_u2"); + """ + ) + ], + state_operations=[ + migrations.RemoveConstraint( + model_name="testtablemain", + name="drop_col_u2", + ), + migrations.RemoveConstraint( + model_name="testtablemain", + name="drop_col_u5", + ), + migrations.RemoveConstraint( + model_name="testtablemain", + name="drop_col_u7", + ), + migrations.RemoveIndex( + model_name="testtablemain", + name="drop_col_i1", + ), + migrations.RemoveIndex( + model_name="testtablemain", + name="drop_col_i2", + ), + migrations.RemoveIndex( + model_name="testtablemain", + name="drop_col_i3", + ), + migrations.RemoveIndex( + model_name="testtablemain", + name="drop_col_i4", + ), + migrations.RemoveIndex( + model_name="testtablemain", + name="drop_col_i5", + ), + migrations.RemoveIndex( + model_name="testtablemain", + name="drop_col_i6", + ), + migrations.RemoveIndex( + model_name="testtablemain", + name="drop_col_i7", + ), + migrations.RemoveField( + model_name="testtablechild", + name="main", + ), + ], + ) + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0003_remove_testtablemain_field_i7.py b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0003_remove_testtablemain_field_i7.py new file mode 100644 index 0000000..9290244 --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0003_remove_testtablemain_field_i7.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ( + "good_flow_drop_column_with_constraints_old", + "0002_remove_testtablemain_drop_col_u2_and_more", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="field_i7", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0004_remove_testtablemain_field_i6.py b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0004_remove_testtablemain_field_i6.py new file mode 100644 index 0000000..fb04a26 --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0004_remove_testtablemain_field_i6.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ( + "good_flow_drop_column_with_constraints_old", + "0003_remove_testtablemain_field_i7", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="field_i6", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0005_remove_testtablemain_field_i5.py b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0005_remove_testtablemain_field_i5.py new file mode 100644 index 0000000..ee3754d --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0005_remove_testtablemain_field_i5.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ( + "good_flow_drop_column_with_constraints_old", + "0004_remove_testtablemain_field_i6", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="field_i5", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0006_remove_testtablemain_field_i4.py b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0006_remove_testtablemain_field_i4.py new file mode 100644 index 0000000..9b167d3 --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0006_remove_testtablemain_field_i4.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ( + "good_flow_drop_column_with_constraints_old", + "0005_remove_testtablemain_field_i5", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="field_i4", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0007_remove_testtablemain_field_i3.py b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0007_remove_testtablemain_field_i3.py new file mode 100644 index 0000000..82b51ee --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0007_remove_testtablemain_field_i3.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ( + "good_flow_drop_column_with_constraints_old", + "0006_remove_testtablemain_field_i4", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="field_i3", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0008_remove_testtablemain_field_i2.py b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0008_remove_testtablemain_field_i2.py new file mode 100644 index 0000000..6c07edd --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0008_remove_testtablemain_field_i2.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ( + "good_flow_drop_column_with_constraints_old", + "0007_remove_testtablemain_field_i3", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="field_i2", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0009_remove_testtablemain_field_i1.py b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0009_remove_testtablemain_field_i1.py new file mode 100644 index 0000000..8f810da --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0009_remove_testtablemain_field_i1.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ( + "good_flow_drop_column_with_constraints_old", + "0008_remove_testtablemain_field_i2", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="field_i1", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0010_remove_testtablemain_field_u7.py b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0010_remove_testtablemain_field_u7.py new file mode 100644 index 0000000..e048959 --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0010_remove_testtablemain_field_u7.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ( + "good_flow_drop_column_with_constraints_old", + "0009_remove_testtablemain_field_i1", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="field_u7", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0011_remove_testtablemain_field_u5.py b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0011_remove_testtablemain_field_u5.py new file mode 100644 index 0000000..b86df4e --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0011_remove_testtablemain_field_u5.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ( + "good_flow_drop_column_with_constraints_old", + "0010_remove_testtablemain_field_u7", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="field_u5", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0012_remove_testtablemain_field_u2.py b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0012_remove_testtablemain_field_u2.py new file mode 100644 index 0000000..e1f393e --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0012_remove_testtablemain_field_u2.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ( + "good_flow_drop_column_with_constraints_old", + "0011_remove_testtablemain_field_u5", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="field_u2", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0013_remove_testtablemain_main_id.py b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0013_remove_testtablemain_main_id.py new file mode 100644 index 0000000..e25e30c --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0013_remove_testtablemain_main_id.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ( + "good_flow_drop_column_with_constraints_old", + "0012_remove_testtablemain_field_u2", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="main_id", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0014_remove_testtablemain_parent.py b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0014_remove_testtablemain_parent.py new file mode 100644 index 0000000..e7a673a --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/0014_remove_testtablemain_parent.py @@ -0,0 +1,20 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ( + "good_flow_drop_column_with_constraints_old", + "0013_remove_testtablemain_main_id", + ), + ] + + operations = [ + migrations.RemoveField( + model_name="testtablemain", + name="parent", + ), + ] diff --git a/tests/apps/good_flow_drop_column_with_constraints_old/migrations/__init__.py b/tests/apps/good_flow_drop_column_with_constraints_old/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/apps/good_flow_drop_column_with_constraints_old/models.py b/tests/apps/good_flow_drop_column_with_constraints_old/models.py new file mode 100644 index 0000000..df7bad0 --- /dev/null +++ b/tests/apps/good_flow_drop_column_with_constraints_old/models.py @@ -0,0 +1,48 @@ +from django.db import models + +# from django.db.models.functions import Abs + + +class TestTableParent(models.Model): + + class Meta: + db_table = 'drop_col_test_table_parent' + + +class TestTableMain(models.Model): + # parent = models.OneToOneField(TestTableParent, null=True, on_delete=models.CASCADE) + # main_id = models.IntegerField(null=True, unique=True) + # field_u2 = models.IntegerField(null=True) + # field_u5 = models.IntegerField(null=True) + # field_u7 = models.IntegerField(null=True) + # field_i1 = models.IntegerField(null=True) + # field_i2 = models.IntegerField(null=True) + # field_i3 = models.IntegerField(null=True) + # field_i4 = models.IntegerField(null=True) + # field_i5 = models.IntegerField(null=True) + # field_i6 = models.IntegerField(null=True) + # field_i7 = models.IntegerField(null=True) + + class Meta: + db_table = 'drop_col_test_table_main' + # constraints = [ + # models.UniqueConstraint(fields=["parent", "field_u2"], name="drop_col_u2"), + # models.UniqueConstraint(fields=["parent"], name="drop_col_u5", condition=models.Q(field_u5__gt=0)), + # models.UniqueConstraint(fields=["parent"], name="drop_col_u7", include=["field_u7"]), + # ] + # indexes = [ + # models.Index("parent", "field_i1", name="drop_col_i1"), + # models.Index(fields=["parent", "field_i2"], name="drop_col_i2"), + # models.Index(Abs("field_i3"), name="drop_col_i3"), + # models.Index("parent", name="drop_col_i4", condition=models.Q(field_i4__gt=0)), + # models.Index(fields=["parent"], name="drop_col_i5", condition=models.Q(field_i5__gt=0)), + # models.Index("parent", name="drop_col_i6", include=["field_i6"]), + # models.Index(fields=["parent"], name="drop_col_i7", include=["field_i7"]), + # ] + + +class TestTableChild(models.Model): + # main = models.ForeignKey(TestTableMain, to_field="main_id", null=True, on_delete=models.CASCADE) + + class Meta: + db_table = 'drop_col_test_table_child' diff --git a/tests/apps/good_flow_drop_table_with_constraints/__init__.py b/tests/apps/good_flow_drop_table_with_constraints/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/apps/good_flow_drop_table_with_constraints/migrations/0001_initial.py b/tests/apps/good_flow_drop_table_with_constraints/migrations/0001_initial.py new file mode 100644 index 0000000..95af8e4 --- /dev/null +++ b/tests/apps/good_flow_drop_table_with_constraints/migrations/0001_initial.py @@ -0,0 +1,82 @@ +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="TestTableMain", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("main_id", models.IntegerField(null=True, unique=True)), + ], + options={ + "db_table": "drop_tbl_test_table_main", + }, + ), + migrations.CreateModel( + name="TestTableParent", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ], + options={ + "db_table": "drop_tbl_test_table_parent", + }, + ), + migrations.CreateModel( + name="TestTableChild", + fields=[ + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "main", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="good_flow_drop_table_with_constraints.testtablemain", + to_field="main_id", + ), + ), + ], + options={ + "db_table": "drop_tbl_test_table_child", + }, + ), + migrations.AddField( + model_name="testtablemain", + name="parent", + field=models.OneToOneField( + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="good_flow_drop_table_with_constraints.testtableparent", + ), + ), + ] diff --git a/tests/apps/good_flow_drop_table_with_constraints/migrations/0002_remove_testtablechild_main.py b/tests/apps/good_flow_drop_table_with_constraints/migrations/0002_remove_testtablechild_main.py new file mode 100644 index 0000000..8c9c8eb --- /dev/null +++ b/tests/apps/good_flow_drop_table_with_constraints/migrations/0002_remove_testtablechild_main.py @@ -0,0 +1,21 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("good_flow_drop_table_with_constraints", "0001_initial"), + ] + + operations = [ + # emulate worst case untracked constraints + migrations.SeparateDatabaseAndState( + database_operations=[], + state_operations=[ + migrations.RemoveField( + model_name="testtablechild", + name="main", + ), + ], + ), + ] diff --git a/tests/apps/good_flow_drop_table_with_constraints/migrations/0003_delete_testtablemain.py b/tests/apps/good_flow_drop_table_with_constraints/migrations/0003_delete_testtablemain.py new file mode 100644 index 0000000..fe894a1 --- /dev/null +++ b/tests/apps/good_flow_drop_table_with_constraints/migrations/0003_delete_testtablemain.py @@ -0,0 +1,16 @@ +from django.db import migrations + + +class Migration(migrations.Migration): + + atomic = False + + dependencies = [ + ("good_flow_drop_table_with_constraints", "0002_remove_testtablechild_main"), + ] + + operations = [ + migrations.DeleteModel( + name="TestTableMain", + ), + ] diff --git a/tests/apps/good_flow_drop_table_with_constraints/migrations/__init__.py b/tests/apps/good_flow_drop_table_with_constraints/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/apps/good_flow_drop_table_with_constraints/models.py b/tests/apps/good_flow_drop_table_with_constraints/models.py new file mode 100644 index 0000000..382568a --- /dev/null +++ b/tests/apps/good_flow_drop_table_with_constraints/models.py @@ -0,0 +1,22 @@ +from django.db import models + + +class TestTableParent(models.Model): + + class Meta: + db_table = 'drop_tbl_test_table_parent' + + +# class TestTableMain(models.Model): +# parent = models.OneToOneField(TestTableParent, null=True, on_delete=models.CASCADE) +# main_id = models.IntegerField(null=True, unique=True) +# +# class Meta: +# db_table = 'drop_tbl_test_table_main' + + +class TestTableChild(models.Model): + # main = models.ForeignKey(TestTableMain, to_field="main_id", null=True, on_delete=models.CASCADE) + + class Meta: + db_table = 'drop_tbl_test_table_child' diff --git a/tests/integration/test_migrations.py b/tests/integration/test_migrations.py index 9650232..30b9a22 100644 --- a/tests/integration/test_migrations.py +++ b/tests/integration/test_migrations.py @@ -153,6 +153,339 @@ def test_decimal_to_float_app(): call_command("migrate", "decimal_to_float_app", "zero") +@skip_for_default_django_backend +@pytest.mark.django_db(transaction=True) +@modify_settings(INSTALLED_APPS={"append": "tests.apps.good_flow_drop_table_with_constraints"}) +@override_settings(ZERO_DOWNTIME_MIGRATIONS_RAISE_FOR_UNSAFE=True) +def test_good_flow_drop_table_with_constraints(): + with override_settings(ZERO_DOWNTIME_MIGRATIONS_EXPLICIT_CONSTRAINTS_DROP=False): + call_command("migrate", "good_flow_drop_table_with_constraints") + drop_tbl_test_table_parent_schema = pg_dump("drop_tbl_test_table_parent") + drop_tbl_test_table_child_schema = pg_dump("drop_tbl_test_table_child") + call_command("migrate", "good_flow_drop_table_with_constraints", "zero") + + _drop_child_foreign_key_constraint_sql = one_line_sql(""" + SET CONSTRAINTS "drop_tbl_test_table__main_id_8a4874b6_fk_drop_tbl_" IMMEDIATE; + ALTER TABLE "drop_tbl_test_table_child" + DROP CONSTRAINT "drop_tbl_test_table__main_id_8a4874b6_fk_drop_tbl_"; + """) + _drop_main_foreign_key_constraint_sql = one_line_sql(""" + SET CONSTRAINTS "drop_tbl_test_table__parent_id_5c6ff8d9_fk_drop_tbl_" IMMEDIATE; + ALTER TABLE "drop_tbl_test_table_main" + DROP CONSTRAINT "drop_tbl_test_table__parent_id_5c6ff8d9_fk_drop_tbl_"; + """) + _drop_table_sql = one_line_sql(""" + DROP TABLE "drop_tbl_test_table_main" CASCADE; + """) + + call_command("migrate", "good_flow_drop_table_with_constraints", "0002") + migration_sql = call_command("sqlmigrate", "good_flow_drop_table_with_constraints", "0003") + assert split_sql_queries(migration_sql) == [ + _drop_child_foreign_key_constraint_sql, + _drop_main_foreign_key_constraint_sql, + _drop_table_sql, + ] + call_command("migrate", "good_flow_drop_table_with_constraints") + assert pg_dump("drop_tbl_test_table_parent") == drop_tbl_test_table_parent_schema + assert pg_dump("drop_tbl_test_table_child") == drop_tbl_test_table_child_schema + + +@skip_for_default_django_backend +@pytest.mark.skipif( + django.VERSION[:2] < (4, 0), + reason="django before 4.0 case", +) +@pytest.mark.django_db(transaction=True) +@modify_settings(INSTALLED_APPS={"append": "tests.apps.good_flow_drop_column_with_constraints"}) +@override_settings(ZERO_DOWNTIME_MIGRATIONS_RAISE_FOR_UNSAFE=True) +def test_good_flow_drop_column_with_constraints(): + with override_settings(ZERO_DOWNTIME_MIGRATIONS_EXPLICIT_CONSTRAINTS_DROP=False): + call_command("migrate", "good_flow_drop_column_with_constraints") + drop_col_test_table_parent_schema = pg_dump("drop_col_test_table_parent") + drop_col_test_table_main_schema = pg_dump("drop_col_test_table_main") + drop_col_test_table_child_schema = pg_dump("drop_col_test_table_child") + call_command("migrate", "good_flow_drop_column_with_constraints", "zero") + + call_command("migrate", "good_flow_drop_column_with_constraints", "0002") + + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints", "0003") + assert split_sql_queries(migration_sql) == [ + 'DROP INDEX CONCURRENTLY IF EXISTS "drop_col_i7";', + 'ALTER TABLE "drop_col_test_table_main" DROP COLUMN "field_i7" CASCADE;', + ] + call_command("migrate", "good_flow_drop_column_with_constraints", "0003") + + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints", "0004") + assert split_sql_queries(migration_sql) == [ + 'DROP INDEX CONCURRENTLY IF EXISTS "drop_col_i6";', + 'ALTER TABLE "drop_col_test_table_main" DROP COLUMN "field_i6" CASCADE;', + ] + call_command("migrate", "good_flow_drop_column_with_constraints", "0004") + + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints", "0005") + assert split_sql_queries(migration_sql) == [ + 'DROP INDEX CONCURRENTLY IF EXISTS "drop_col_i5";', + 'ALTER TABLE "drop_col_test_table_main" DROP COLUMN "field_i5" CASCADE;', + ] + call_command("migrate", "good_flow_drop_column_with_constraints", "0005") + + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints", "0006") + assert split_sql_queries(migration_sql) == [ + 'DROP INDEX CONCURRENTLY IF EXISTS "drop_col_i4";', + 'ALTER TABLE "drop_col_test_table_main" DROP COLUMN "field_i4" CASCADE;', + ] + call_command("migrate", "good_flow_drop_column_with_constraints", "0006") + + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints", "0007") + assert split_sql_queries(migration_sql) == [ + 'DROP INDEX CONCURRENTLY IF EXISTS "drop_col_i3";', + 'ALTER TABLE "drop_col_test_table_main" DROP COLUMN "field_i3" CASCADE;', + ] + call_command("migrate", "good_flow_drop_column_with_constraints", "0007") + + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints", "0008") + assert split_sql_queries(migration_sql) == [ + 'DROP INDEX CONCURRENTLY IF EXISTS "drop_col_i2";', + 'ALTER TABLE "drop_col_test_table_main" DROP COLUMN "field_i2" CASCADE;', + ] + call_command("migrate", "good_flow_drop_column_with_constraints", "0008") + + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints", "0009") + assert split_sql_queries(migration_sql) == [ + 'DROP INDEX CONCURRENTLY IF EXISTS "drop_col_i1";', + 'ALTER TABLE "drop_col_test_table_main" DROP COLUMN "field_i1" CASCADE;', + ] + call_command("migrate", "good_flow_drop_column_with_constraints", "0009") + + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints", "0010") + assert split_sql_queries(migration_sql) == [ + 'DROP INDEX CONCURRENTLY IF EXISTS "drop_col_u7";', + 'ALTER TABLE "drop_col_test_table_main" DROP COLUMN "field_u7" CASCADE;', + ] + call_command("migrate", "good_flow_drop_column_with_constraints", "0010") + + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints", "0011") + assert split_sql_queries(migration_sql) == [ + 'DROP INDEX CONCURRENTLY IF EXISTS "drop_col_u6";', + 'ALTER TABLE "drop_col_test_table_main" DROP COLUMN "field_u6" CASCADE;', + ] + call_command("migrate", "good_flow_drop_column_with_constraints", "0011") + + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints", "0012") + assert split_sql_queries(migration_sql) == [ + 'DROP INDEX CONCURRENTLY IF EXISTS "drop_col_u5";', + 'ALTER TABLE "drop_col_test_table_main" DROP COLUMN "field_u5" CASCADE;', + ] + call_command("migrate", "good_flow_drop_column_with_constraints", "0012") + + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints", "0013") + assert split_sql_queries(migration_sql) == [ + 'DROP INDEX CONCURRENTLY IF EXISTS "drop_col_u4";', + 'ALTER TABLE "drop_col_test_table_main" DROP COLUMN "field_u4" CASCADE;', + ] + call_command("migrate", "good_flow_drop_column_with_constraints", "0013") + + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints", "0014") + assert split_sql_queries(migration_sql) == [ + 'DROP INDEX CONCURRENTLY IF EXISTS "drop_col_u3";', + 'ALTER TABLE "drop_col_test_table_main" DROP COLUMN "field_u3" CASCADE;', + ] + call_command("migrate", "good_flow_drop_column_with_constraints", "0014") + + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints", "0015") + assert split_sql_queries(migration_sql) == [ + 'ALTER TABLE "drop_col_test_table_main" DROP CONSTRAINT "drop_col_u2";', + 'ALTER TABLE "drop_col_test_table_main" DROP COLUMN "field_u2" CASCADE;', + ] + call_command("migrate", "good_flow_drop_column_with_constraints", "0015") + + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints", "0016") + assert split_sql_queries(migration_sql) == [ + 'DROP INDEX CONCURRENTLY IF EXISTS "drop_col_u1";', + 'ALTER TABLE "drop_col_test_table_main" DROP COLUMN "field_u1" CASCADE;', + ] + call_command("migrate", "good_flow_drop_column_with_constraints", "0016") + + _drop_main_id_child_foreign_key_constraint_sql = one_line_sql(""" + SET CONSTRAINTS "drop_col_test_table__main_id_9da91a1c_fk_drop_col_" IMMEDIATE; + ALTER TABLE "drop_col_test_table_child" + DROP CONSTRAINT "drop_col_test_table__main_id_9da91a1c_fk_drop_col_"; + """) + _drop_main_id_field_unique_constraint = one_line_sql(""" + ALTER TABLE "drop_col_test_table_main" + DROP CONSTRAINT "drop_col_test_table_main_main_id_key"; + """) + _drop_main_id_column_sql = one_line_sql(""" + ALTER TABLE "drop_col_test_table_main" DROP COLUMN "main_id" CASCADE; + """) + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints", "0017") + assert split_sql_queries(migration_sql) == [ + _drop_main_id_child_foreign_key_constraint_sql, + _drop_main_id_field_unique_constraint, + _drop_main_id_column_sql, + ] + call_command("migrate", "good_flow_drop_column_with_constraints", "0017") + + _drop_parent_id_main_foreign_key_constraint_sql = one_line_sql(""" + SET CONSTRAINTS "drop_col_test_table__parent_id_55b0b5e6_fk_drop_col_" IMMEDIATE; + ALTER TABLE "drop_col_test_table_main" + DROP CONSTRAINT "drop_col_test_table__parent_id_55b0b5e6_fk_drop_col_"; + """) + _drop_parent_id_field_unique_constraint = one_line_sql(""" + ALTER TABLE "drop_col_test_table_main" + DROP CONSTRAINT "drop_col_test_table_main_parent_id_55b0b5e6_uniq"; + """) + _drop_parent_id_column_sql = one_line_sql(""" + ALTER TABLE "drop_col_test_table_main" DROP COLUMN "parent_id" CASCADE; + """) + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints", "0018") + assert split_sql_queries(migration_sql) == [ + _drop_parent_id_main_foreign_key_constraint_sql, + _drop_parent_id_field_unique_constraint, + _drop_parent_id_column_sql, + ] + call_command("migrate", "good_flow_drop_column_with_constraints", "0018") + + call_command("migrate", "good_flow_drop_column_with_constraints") + assert pg_dump("drop_col_test_table_parent") == drop_col_test_table_parent_schema + assert pg_dump("drop_col_test_table_main") == drop_col_test_table_main_schema + assert pg_dump("drop_col_test_table_child") == drop_col_test_table_child_schema + + +@skip_for_default_django_backend +@pytest.mark.skipif( + django.VERSION[:2] >= (4, 0), + reason="django after 4.0 case", +) +@pytest.mark.django_db(transaction=True) +@modify_settings(INSTALLED_APPS={"append": "tests.apps.good_flow_drop_column_with_constraints_old"}) +@override_settings(ZERO_DOWNTIME_MIGRATIONS_RAISE_FOR_UNSAFE=True) +def test_good_flow_drop_column_with_constraints_old(): + with override_settings(ZERO_DOWNTIME_MIGRATIONS_EXPLICIT_CONSTRAINTS_DROP=False): + call_command("migrate", "good_flow_drop_column_with_constraints_old") + drop_col_test_table_parent_schema = pg_dump("drop_col_test_table_parent") + drop_col_test_table_main_schema = pg_dump("drop_col_test_table_main") + drop_col_test_table_child_schema = pg_dump("drop_col_test_table_child") + call_command("migrate", "good_flow_drop_column_with_constraints_old", "zero") + + call_command("migrate", "good_flow_drop_column_with_constraints_old", "0002") + + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints_old", "0003") + assert split_sql_queries(migration_sql) == [ + 'DROP INDEX CONCURRENTLY IF EXISTS "drop_col_i7";', + 'ALTER TABLE "drop_col_test_table_main" DROP COLUMN "field_i7" CASCADE;', + ] + call_command("migrate", "good_flow_drop_column_with_constraints_old", "0003") + + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints_old", "0004") + assert split_sql_queries(migration_sql) == [ + 'DROP INDEX CONCURRENTLY IF EXISTS "drop_col_i6";', + 'ALTER TABLE "drop_col_test_table_main" DROP COLUMN "field_i6" CASCADE;', + ] + call_command("migrate", "good_flow_drop_column_with_constraints_old", "0004") + + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints_old", "0005") + assert split_sql_queries(migration_sql) == [ + 'DROP INDEX CONCURRENTLY IF EXISTS "drop_col_i5";', + 'ALTER TABLE "drop_col_test_table_main" DROP COLUMN "field_i5" CASCADE;', + ] + call_command("migrate", "good_flow_drop_column_with_constraints_old", "0005") + + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints_old", "0006") + assert split_sql_queries(migration_sql) == [ + 'DROP INDEX CONCURRENTLY IF EXISTS "drop_col_i4";', + 'ALTER TABLE "drop_col_test_table_main" DROP COLUMN "field_i4" CASCADE;', + ] + call_command("migrate", "good_flow_drop_column_with_constraints_old", "0006") + + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints_old", "0007") + assert split_sql_queries(migration_sql) == [ + 'DROP INDEX CONCURRENTLY IF EXISTS "drop_col_i3";', + 'ALTER TABLE "drop_col_test_table_main" DROP COLUMN "field_i3" CASCADE;', + ] + call_command("migrate", "good_flow_drop_column_with_constraints_old", "0007") + + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints_old", "0008") + assert split_sql_queries(migration_sql) == [ + 'DROP INDEX CONCURRENTLY IF EXISTS "drop_col_i2";', + 'ALTER TABLE "drop_col_test_table_main" DROP COLUMN "field_i2" CASCADE;', + ] + call_command("migrate", "good_flow_drop_column_with_constraints_old", "0008") + + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints_old", "0009") + assert split_sql_queries(migration_sql) == [ + 'DROP INDEX CONCURRENTLY IF EXISTS "drop_col_i1";', + 'ALTER TABLE "drop_col_test_table_main" DROP COLUMN "field_i1" CASCADE;', + ] + call_command("migrate", "good_flow_drop_column_with_constraints_old", "0009") + + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints_old", "0010") + assert split_sql_queries(migration_sql) == [ + 'DROP INDEX CONCURRENTLY IF EXISTS "drop_col_u7";', + 'ALTER TABLE "drop_col_test_table_main" DROP COLUMN "field_u7" CASCADE;', + ] + call_command("migrate", "good_flow_drop_column_with_constraints_old", "0010") + + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints_old", "0011") + assert split_sql_queries(migration_sql) == [ + 'DROP INDEX CONCURRENTLY IF EXISTS "drop_col_u5";', + 'ALTER TABLE "drop_col_test_table_main" DROP COLUMN "field_u5" CASCADE;', + ] + call_command("migrate", "good_flow_drop_column_with_constraints_old", "0011") + + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints_old", "0012") + assert split_sql_queries(migration_sql) == [ + 'ALTER TABLE "drop_col_test_table_main" DROP CONSTRAINT "drop_col_u2";', + 'ALTER TABLE "drop_col_test_table_main" DROP COLUMN "field_u2" CASCADE;', + ] + call_command("migrate", "good_flow_drop_column_with_constraints_old", "0012") + + _drop_main_id_child_foreign_key_constraint_sql = one_line_sql(""" + SET CONSTRAINTS "drop_col_test_table__main_id_9da91a1c_fk_drop_col_" IMMEDIATE; + ALTER TABLE "drop_col_test_table_child" + DROP CONSTRAINT "drop_col_test_table__main_id_9da91a1c_fk_drop_col_"; + """) + _drop_main_id_field_unique_constraint = one_line_sql(""" + ALTER TABLE "drop_col_test_table_main" + DROP CONSTRAINT "drop_col_test_table_main_main_id_key"; + """) + _drop_main_id_column_sql = one_line_sql(""" + ALTER TABLE "drop_col_test_table_main" DROP COLUMN "main_id" CASCADE; + """) + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints_old", "0013") + assert split_sql_queries(migration_sql) == [ + _drop_main_id_child_foreign_key_constraint_sql, + _drop_main_id_field_unique_constraint, + _drop_main_id_column_sql, + ] + call_command("migrate", "good_flow_drop_column_with_constraints_old", "0013") + + _drop_parent_id_main_foreign_key_constraint_sql = one_line_sql(""" + SET CONSTRAINTS "drop_col_test_table__parent_id_55b0b5e6_fk_drop_col_" IMMEDIATE; + ALTER TABLE "drop_col_test_table_main" + DROP CONSTRAINT "drop_col_test_table__parent_id_55b0b5e6_fk_drop_col_"; + """) + _drop_parent_id_field_unique_constraint = one_line_sql(""" + ALTER TABLE "drop_col_test_table_main" + DROP CONSTRAINT "drop_col_test_table_main_parent_id_55b0b5e6_uniq"; + """) + _drop_parent_id_column_sql = one_line_sql(""" + ALTER TABLE "drop_col_test_table_main" DROP COLUMN "parent_id" CASCADE; + """) + migration_sql = call_command("sqlmigrate", "good_flow_drop_column_with_constraints_old", "0014") + assert split_sql_queries(migration_sql) == [ + _drop_parent_id_main_foreign_key_constraint_sql, + _drop_parent_id_field_unique_constraint, + _drop_parent_id_column_sql, + ] + call_command("migrate", "good_flow_drop_column_with_constraints_old", "0014") + + call_command("migrate", "good_flow_drop_column_with_constraints_old") + assert pg_dump("drop_col_test_table_parent") == drop_col_test_table_parent_schema + assert pg_dump("drop_col_test_table_main") == drop_col_test_table_main_schema + assert pg_dump("drop_col_test_table_child") == drop_col_test_table_child_schema + + @pytest.mark.django_db(transaction=True) @pytest.mark.skipif( settings.DATABASES["default"]["ENGINE"] != "django_zero_downtime_migrations.backends.postgres", diff --git a/tests/settings_make_migrations.py b/tests/settings_make_migrations.py index 27bfbd2..3d565da 100644 --- a/tests/settings_make_migrations.py +++ b/tests/settings_make_migrations.py @@ -12,6 +12,8 @@ 'tests.apps.bad_flow_add_column_with_notnull_app', 'tests.apps.bad_flow_change_char_type_that_unsafe_app', 'tests.apps.old_notnull_check_constraint_migration_app', + 'tests.apps.good_flow_drop_table_with_constraints', + 'tests.apps.good_flow_drop_column_with_constraints', 'tests.apps.idempotency_create_table_app', 'tests.apps.idempotency_add_column_app', 'tests.apps.idempotency_add_column_foreign_key_app', diff --git a/tests/unit/test_schema.py b/tests/unit/test_schema.py index 448c28e..873e7a2 100644 --- a/tests/unit/test_schema.py +++ b/tests/unit/test_schema.py @@ -135,6 +135,46 @@ def test_drop_model__ok(): ] +@pytest.mark.django_db +@override_settings(ZERO_DOWNTIME_MIGRATIONS_RAISE_FOR_UNSAFE=True) +def test_drop_model__drop_foreign_key__ok(cursor, mocker): + mocker.patch.object(cursor, 'execute') + mocker.patch.object(cursor, 'fetchall').return_value = [ + ('tests_model_model2_id_fk', 'f', 'tests_model', 'tests_model2', ['model2_id'], ['id']) + ] + with cmp_schema_editor() as editor: + editor.delete_model(Model) + assert editor.collected_sql == timeouts( + 'SET CONSTRAINTS "tests_model_model2_id_fk" IMMEDIATE; ' + 'ALTER TABLE "tests_model" DROP CONSTRAINT "tests_model_model2_id_fk";', + ) + [ + 'DROP TABLE "tests_model" CASCADE;' + ] + assert editor.django_sql == [ + 'DROP TABLE "tests_model" CASCADE;', + ] + + +@pytest.mark.django_db +@override_settings(ZERO_DOWNTIME_MIGRATIONS_RAISE_FOR_UNSAFE=True) +def test_drop_model__drop_foreign_key_backref__ok(cursor, mocker): + mocker.patch.object(cursor, 'execute') + mocker.patch.object(cursor, 'fetchall').return_value = [ + ('tests_model2_model_id_fk', 'f', 'tests_model2', 'tests_model', ['model_id'], ['id']) + ] + with cmp_schema_editor() as editor: + editor.delete_model(Model) + assert editor.collected_sql == timeouts( + 'SET CONSTRAINTS "tests_model2_model_id_fk" IMMEDIATE; ' + 'ALTER TABLE "tests_model2" DROP CONSTRAINT "tests_model2_model_id_fk";', + ) + [ + 'DROP TABLE "tests_model" CASCADE;' + ] + assert editor.django_sql == [ + 'DROP TABLE "tests_model" CASCADE;', + ] + + @pytest.mark.django_db def test_rename_model__warning(): with cmp_schema_editor() as editor: @@ -1164,6 +1204,146 @@ def test_remove_field__ok(): ] +@pytest.mark.django_db +@override_settings(ZERO_DOWNTIME_MIGRATIONS_RAISE_FOR_UNSAFE=True) +def test_remove_field_with_foreign_key__ok(cursor, mocker): + mocker.patch.object(cursor, 'execute') + mocker.patch.object(cursor, 'fetchall').side_effect = [ + [ + ('tests_model_field_fk', 'f', 'tests_model', 'tests_model2', ['field'], ['id']), + ('tests_model_field2_fk', 'f', 'tests_model', 'tests_model2', ['field2'], ['id']), + ], + [ + ('tests_model_field2_fk', 'f', 'tests_model', 'tests_model2', ['field2'], ['id']), + ], + [], + ] + with cmp_schema_editor() as editor: + field = models.CharField(max_length=40) + field.set_attributes_from_name('field') + editor.remove_field(Model, field) + assert editor.collected_sql == timeouts( + 'SET CONSTRAINTS "tests_model_field_fk" IMMEDIATE; ' + 'ALTER TABLE "tests_model" DROP CONSTRAINT "tests_model_field_fk";' + ) + timeouts( + 'ALTER TABLE "tests_model" DROP COLUMN "field" CASCADE;' + ) + assert editor.django_sql == [ + 'ALTER TABLE "tests_model" DROP COLUMN "field" CASCADE;', + ] + + +@pytest.mark.django_db +@override_settings(ZERO_DOWNTIME_MIGRATIONS_RAISE_FOR_UNSAFE=True) +def test_remove_field_with_foreign_key_backref__ok(cursor, mocker): + mocker.patch.object(cursor, 'execute') + mocker.patch.object(cursor, 'fetchall').side_effect = [ + [ + ('tests_model2_model_field_fk', 'f', 'tests_model2', 'tests_model', ['model_field'], ['field']), + ('tests_model2_model_field2_fk', 'f', 'tests_model2', 'tests_model', ['model_field2'], ['field2']), + ], + [ + ('tests_model2_model_field2_fk', 'f', 'tests_model2', 'tests_model', ['model_field2'], ['field2']), + ], + [], + ] + with cmp_schema_editor() as editor: + field = models.CharField(max_length=40) + field.set_attributes_from_name('field') + editor.remove_field(Model, field) + assert editor.collected_sql == timeouts( + 'SET CONSTRAINTS "tests_model2_model_field_fk" IMMEDIATE; ' + 'ALTER TABLE "tests_model2" DROP CONSTRAINT "tests_model2_model_field_fk";' + ) + timeouts( + 'ALTER TABLE "tests_model" DROP COLUMN "field" CASCADE;' + ) + assert editor.django_sql == [ + 'ALTER TABLE "tests_model" DROP COLUMN "field" CASCADE;', + ] + + +@pytest.mark.django_db +@override_settings(ZERO_DOWNTIME_MIGRATIONS_RAISE_FOR_UNSAFE=True) +def test_remove_field_with_unique_constraint__ok(cursor, mocker): + mocker.patch.object(cursor, 'execute') + mocker.patch.object(cursor, 'fetchall').side_effect = [ + [ + ('tests_model_field2_field_uniq', 'u', 'tests_model', None, ['field2', 'field'], []), + ('tests_model_field2_field3_uniq', 'u', 'tests_model', None, ['field2', 'field3'], []), + ], + [ + ('tests_model_field2_field_uniq', 'u', 'tests_model', None, ['field2', 'field'], []), + ('tests_model_field2_field3_uniq', 'u', 'tests_model', None, ['field2', 'field3'], []), + ], + [], + ] + with cmp_schema_editor() as editor: + field = models.CharField(max_length=40) + field.set_attributes_from_name('field') + editor.remove_field(Model, field) + assert editor.collected_sql == timeouts( + 'ALTER TABLE "tests_model" DROP CONSTRAINT "tests_model_field2_field_uniq";', + ) + timeouts( + 'ALTER TABLE "tests_model" DROP COLUMN "field" CASCADE;', + ) + assert editor.django_sql == [ + 'ALTER TABLE "tests_model" DROP COLUMN "field" CASCADE;', + ] + + +@pytest.mark.django_db +@override_settings(ZERO_DOWNTIME_MIGRATIONS_RAISE_FOR_UNSAFE=True) +def test_remove_field_with_index__ok(cursor, mocker): + mocker.patch.object(cursor, 'execute') + mocker.patch.object(cursor, 'fetchall').side_effect = [ + [], + [], + [ + ('tests_model_field2_field_idx', 'tests_model', ['field2', 'field']), + ('tests_model_field2_field3_idx', 'tests_model', ['field2', 'field3']), + ], + ] + with cmp_schema_editor() as editor: + field = models.CharField(max_length=40) + field.set_attributes_from_name('field') + editor.remove_field(Model, field) + assert editor.collected_sql == [ + 'DROP INDEX CONCURRENTLY IF EXISTS "tests_model_field2_field_idx";', + ] + timeouts( + 'ALTER TABLE "tests_model" DROP COLUMN "field" CASCADE;', + ) + assert editor.django_sql == [ + 'ALTER TABLE "tests_model" DROP COLUMN "field" CASCADE;', + ] + + +@pytest.mark.django_db +@override_settings(ZERO_DOWNTIME_MIGRATIONS_RAISE_FOR_UNSAFE=True, + ZERO_DOWNTIME_MIGRATIONS_FLEXIBLE_STATEMENT_TIMEOUT=True) +def test_remove_field_with_index__with_flexible_timeout__ok(cursor, mocker): + mocker.patch.object(cursor, 'execute') + mocker.patch.object(cursor, 'fetchall').side_effect = [ + [], + [], + [ + ('tests_model_field2_field_idx', 'tests_model', ['field2', 'field']), + ('tests_model_field2_field3_idx', 'tests_model', ['field2', 'field3']), + ], + ] + with cmp_schema_editor() as editor: + field = models.CharField(max_length=40) + field.set_attributes_from_name('field') + editor.remove_field(Model, field) + assert editor.collected_sql == flexible_statement_timeout( + 'DROP INDEX CONCURRENTLY IF EXISTS "tests_model_field2_field_idx";', + ) + timeouts( + 'ALTER TABLE "tests_model" DROP COLUMN "field" CASCADE;', + ) + assert editor.django_sql == [ + 'ALTER TABLE "tests_model" DROP COLUMN "field" CASCADE;', + ] + + @pytest.mark.django_db @override_settings(ZERO_DOWNTIME_MIGRATIONS_RAISE_FOR_UNSAFE=True) def test_alter_field_add_constraint_check__ok(): @@ -1285,7 +1465,7 @@ def test_alter_filed_add_constraint_foreign_key__with_flexible_timeout__ok(): @override_settings(ZERO_DOWNTIME_MIGRATIONS_RAISE_FOR_UNSAFE=True) def test_alter_field_drop_constraint_foreign_key__ok(mocker): mocker.patch.object(connection.introspection, 'get_constraints').return_value = { - 'tests_model_field_0a53d95f_pk': { + 'tests_model_field_0a53d95f_fk': { 'columns': ['field_id'], 'primary_key': False, 'unique': False, @@ -1304,8 +1484,8 @@ def test_alter_field_drop_constraint_foreign_key__ok(mocker): editor.alter_field(Model, old_field, new_field) assert editor.collected_sql == timeouts(editor.django_sql) assert editor.django_sql == [ - 'SET CONSTRAINTS "tests_model_field_0a53d95f_pk" IMMEDIATE; ' - 'ALTER TABLE "tests_model" DROP CONSTRAINT "tests_model_field_0a53d95f_pk";', + 'SET CONSTRAINTS "tests_model_field_0a53d95f_fk" IMMEDIATE; ' + 'ALTER TABLE "tests_model" DROP CONSTRAINT "tests_model_field_0a53d95f_fk";', ]