Skip to content

Commit f4eb8e9

Browse files
committed
feat(migrations): Add in SafeDeleteModel migration operation
This adds in the `SafeDeleteModel` operation. Most of this pr is testing and monkeypatching Django migrations to be able to hook this in. `SafeDeleteModel` allows us to delete models in two steps without using custom sql. If accepts a deletion_action parameter, which must be passed, and can be either MOVE_TO_PENDING or DELETE. MOVE_TO_PENDING checks that there are no constraints on the table and removes the table from the migation state. We keep track of pending models separately, so that they can be deleted in the next step. DELETE runs the delete command on the database. It is able to still reference the model since we keep track of this in our patched state. If delete is run before MOVE_TO_PENDING then the migration will be rejected. Some care is still needed when using these - we still have to ensure that the PENDING migration runs and fully deploys before the DELETE does. But other than that this reduces the manual error prone sql work around this process a lot.
1 parent a0db9cf commit f4eb8e9

File tree

33 files changed

+544
-9
lines changed

33 files changed

+544
-9
lines changed

fixtures/safe_migrations_apps/bad_flow_delete_model_double_pending_app/__init__.py

Whitespace-only changes.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Generated by Django 3.1 on 2019-09-22 21:47
2+
3+
from django.db import migrations, models
4+
5+
from sentry.new_migrations.migrations import CheckedMigration
6+
7+
8+
class Migration(CheckedMigration):
9+
10+
initial = True
11+
12+
dependencies = []
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name="TestTable",
17+
fields=[
18+
(
19+
"id",
20+
models.AutoField(
21+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
22+
),
23+
),
24+
],
25+
),
26+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from sentry.new_migrations.migrations import CheckedMigration
2+
from sentry.new_migrations.monkey.models import SafeDeleteModel
3+
from sentry.new_migrations.monkey.state import DeletionAction
4+
5+
6+
class Migration(CheckedMigration):
7+
8+
dependencies = [
9+
("bad_flow_delete_model_double_pending_app", "0001_initial"),
10+
]
11+
12+
operations = [
13+
SafeDeleteModel(
14+
name="TestTable",
15+
deletion_action=DeletionAction.MOVE_TO_PENDING,
16+
),
17+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from sentry.new_migrations.migrations import CheckedMigration
2+
from sentry.new_migrations.monkey.models import SafeDeleteModel
3+
from sentry.new_migrations.monkey.state import DeletionAction
4+
5+
6+
class Migration(CheckedMigration):
7+
8+
dependencies = [
9+
("bad_flow_delete_model_double_pending_app", "0002_delete_pending"),
10+
]
11+
12+
operations = [
13+
SafeDeleteModel(
14+
name="TestTable",
15+
deletion_action=DeletionAction.MOVE_TO_PENDING,
16+
),
17+
]

fixtures/safe_migrations_apps/bad_flow_delete_model_double_pending_app/migrations/__init__.py

Whitespace-only changes.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from django.db import models
2+
3+
4+
class TestTable(models.Model):
5+
field = models.IntegerField(default=0, null=False)

fixtures/safe_migrations_apps/bad_flow_delete_model_without_pending_app/__init__.py

Whitespace-only changes.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Generated by Django 3.1 on 2019-09-22 21:47
2+
3+
from django.db import migrations, models
4+
5+
from sentry.new_migrations.migrations import CheckedMigration
6+
7+
8+
class Migration(CheckedMigration):
9+
10+
initial = True
11+
12+
dependencies = []
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name="TestTable",
17+
fields=[
18+
(
19+
"id",
20+
models.AutoField(
21+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
22+
),
23+
),
24+
],
25+
),
26+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from sentry.new_migrations.migrations import CheckedMigration
2+
from sentry.new_migrations.monkey.models import SafeDeleteModel
3+
from sentry.new_migrations.monkey.state import DeletionAction
4+
5+
6+
class Migration(CheckedMigration):
7+
8+
dependencies = [
9+
("bad_flow_delete_model_without_pending_app", "0001_initial"),
10+
]
11+
12+
operations = [
13+
SafeDeleteModel(
14+
name="TestTable",
15+
deletion_action=DeletionAction.DELETE,
16+
),
17+
]

fixtures/safe_migrations_apps/bad_flow_delete_model_without_pending_app/migrations/__init__.py

Whitespace-only changes.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from django.db import models
2+
3+
4+
class TestTable(models.Model):
5+
field = models.IntegerField(default=0, null=False)

fixtures/safe_migrations_apps/bad_flow_delete_pending_with_fk_constraints_app/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import django
2+
from django.db import migrations, models
3+
4+
import sentry
5+
from sentry.new_migrations.migrations import CheckedMigration
6+
7+
8+
class Migration(CheckedMigration):
9+
10+
initial = True
11+
12+
dependencies = []
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name="FkTable",
17+
fields=[
18+
(
19+
"id",
20+
models.AutoField(
21+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
22+
),
23+
),
24+
],
25+
),
26+
migrations.CreateModel(
27+
name="TestTable",
28+
fields=[
29+
(
30+
"id",
31+
models.AutoField(
32+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
33+
),
34+
),
35+
(
36+
"fk_table",
37+
sentry.db.models.fields.foreignkey.FlexibleForeignKey(
38+
on_delete=django.db.models.deletion.CASCADE,
39+
to="bad_flow_delete_pending_with_fk_constraints_app.fktable",
40+
db_index=False,
41+
),
42+
),
43+
],
44+
),
45+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from sentry.new_migrations.migrations import CheckedMigration
2+
from sentry.new_migrations.monkey.models import SafeDeleteModel
3+
from sentry.new_migrations.monkey.state import DeletionAction
4+
5+
6+
class Migration(CheckedMigration):
7+
atomic = False
8+
9+
dependencies = [
10+
("bad_flow_delete_pending_with_fk_constraints_app", "0001_initial"),
11+
]
12+
13+
operations = [
14+
SafeDeleteModel(
15+
name="TestTable",
16+
deletion_action=DeletionAction.MOVE_TO_PENDING,
17+
),
18+
]

fixtures/safe_migrations_apps/bad_flow_delete_pending_with_fk_constraints_app/migrations/__init__.py

Whitespace-only changes.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from django.db import models
2+
3+
from sentry.db.models import FlexibleForeignKey
4+
5+
6+
class FkTable(models.Model):
7+
field = models.IntegerField(default=0, null=False)
8+
9+
10+
class TestTable(models.Model):
11+
field = models.IntegerField(default=0, null=False)
12+
fk_table = FlexibleForeignKey(FkTable, db_index=False)

fixtures/safe_migrations_apps/good_flow_delete_pending_with_fk_constraints_app/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Generated by Django 3.1 on 2019-09-22 21:47
2+
import django
3+
from django.db import migrations, models
4+
5+
import sentry
6+
from sentry.new_migrations.migrations import CheckedMigration
7+
8+
9+
class Migration(CheckedMigration):
10+
11+
initial = True
12+
13+
dependencies = []
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name="FkTable",
18+
fields=[
19+
(
20+
"id",
21+
models.AutoField(
22+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
23+
),
24+
),
25+
],
26+
),
27+
migrations.CreateModel(
28+
name="TestTable",
29+
fields=[
30+
(
31+
"id",
32+
models.AutoField(
33+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
34+
),
35+
),
36+
(
37+
"fk_table",
38+
sentry.db.models.fields.foreignkey.FlexibleForeignKey(
39+
on_delete=django.db.models.deletion.CASCADE,
40+
to="good_flow_delete_pending_with_fk_constraints_app.fktable",
41+
db_index=False,
42+
),
43+
),
44+
],
45+
),
46+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import django
2+
from django.db import migrations
3+
4+
import sentry
5+
from sentry.new_migrations.migrations import CheckedMigration
6+
from sentry.new_migrations.monkey.models import SafeDeleteModel
7+
from sentry.new_migrations.monkey.state import DeletionAction
8+
9+
10+
class Migration(CheckedMigration):
11+
atomic = False
12+
13+
dependencies = [
14+
("good_flow_delete_pending_with_fk_constraints_app", "0001_initial"),
15+
]
16+
17+
operations = [
18+
migrations.AlterField(
19+
model_name="TestTable",
20+
name="fk_table",
21+
field=sentry.db.models.fields.foreignkey.FlexibleForeignKey(
22+
on_delete=django.db.models.deletion.CASCADE,
23+
to="good_flow_delete_pending_with_fk_constraints_app.fktable",
24+
db_index=False,
25+
db_constraint=False,
26+
),
27+
),
28+
SafeDeleteModel(
29+
name="TestTable",
30+
deletion_action=DeletionAction.MOVE_TO_PENDING,
31+
),
32+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from sentry.new_migrations.migrations import CheckedMigration
2+
from sentry.new_migrations.monkey.models import SafeDeleteModel
3+
from sentry.new_migrations.monkey.state import DeletionAction
4+
5+
6+
class Migration(CheckedMigration):
7+
dependencies = [
8+
("good_flow_delete_pending_with_fk_constraints_app", "0002_remove_constraints_and_pending"),
9+
]
10+
11+
operations = [
12+
SafeDeleteModel(
13+
name="TestTable",
14+
deletion_action=DeletionAction.DELETE,
15+
),
16+
]

fixtures/safe_migrations_apps/good_flow_delete_pending_with_fk_constraints_app/migrations/__init__.py

Whitespace-only changes.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from django.db import models
2+
3+
from sentry.db.models import FlexibleForeignKey
4+
5+
6+
class FkTable(models.Model):
7+
field = models.IntegerField(default=0, null=False)
8+
9+
10+
class TestTable(models.Model):
11+
field = models.IntegerField(default=0, null=False)
12+
fk_table = FlexibleForeignKey(FkTable, db_index=False)

fixtures/safe_migrations_apps/good_flow_delete_simple_app/__init__.py

Whitespace-only changes.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from django.db import migrations, models
2+
3+
from sentry.new_migrations.migrations import CheckedMigration
4+
5+
6+
class Migration(CheckedMigration):
7+
initial = True
8+
dependencies = []
9+
10+
operations = [
11+
migrations.CreateModel(
12+
name="TestTable",
13+
fields=[
14+
(
15+
"id",
16+
models.AutoField(
17+
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
18+
),
19+
),
20+
],
21+
),
22+
]
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from sentry.new_migrations.migrations import CheckedMigration
2+
from sentry.new_migrations.monkey.models import SafeDeleteModel
3+
from sentry.new_migrations.monkey.state import DeletionAction
4+
5+
6+
class Migration(CheckedMigration):
7+
dependencies = [
8+
("good_flow_delete_simple_app", "0001_initial"),
9+
]
10+
11+
operations = [
12+
SafeDeleteModel(
13+
name="TestTable",
14+
deletion_action=DeletionAction.MOVE_TO_PENDING,
15+
),
16+
]
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from sentry.new_migrations.migrations import CheckedMigration
2+
from sentry.new_migrations.monkey.models import SafeDeleteModel
3+
from sentry.new_migrations.monkey.state import DeletionAction
4+
5+
6+
class Migration(CheckedMigration):
7+
dependencies = [
8+
("good_flow_delete_simple_app", "0002_set_pending"),
9+
]
10+
11+
operations = [
12+
SafeDeleteModel(
13+
name="TestTable",
14+
deletion_action=DeletionAction.DELETE,
15+
),
16+
]

fixtures/safe_migrations_apps/good_flow_delete_simple_app/migrations/__init__.py

Whitespace-only changes.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from django.db import models
2+
3+
4+
class TestTable(models.Model):
5+
field = models.IntegerField(default=0, null=False)

src/sentry/db/postgres/schema.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,16 @@ def alter_db_table(self, model, old_db_table, new_db_table):
8888
"More info here: https://develop.sentry.dev/database-migrations/#renaming-tables"
8989
)
9090

91-
def delete_model(self, model):
91+
def delete_model(self, model, is_safe=False):
9292
"""
9393
It's never safe to delete a model using the standard migration process
9494
"""
95-
raise UnsafeOperationException(
96-
f"Deleting the {model.__name__} model is unsafe.\n"
97-
"More info here: https://develop.sentry.dev/database-migrations/#deleting-tables"
98-
)
95+
if not is_safe:
96+
raise UnsafeOperationException(
97+
f"Deleting the {model.__name__} model is unsafe.\n"
98+
"More info here: https://develop.sentry.dev/database-migrations/#deleting-tables"
99+
)
100+
super(DatabaseSchemaEditorMixin, self).delete_model(model)
99101

100102
def remove_field(self, model, field):
101103
"""

0 commit comments

Comments
 (0)