Skip to content

Commit cbd30f7

Browse files
authored
fix table creation ordering in migrations (piccolo-orm#211)
* fix table creation ordering * put a recursion limit on `_compare_tables`
1 parent ff831cf commit cbd30f7

File tree

3 files changed

+89
-20
lines changed

3 files changed

+89
-20
lines changed

piccolo/apps/migrations/auto/migration_manager.py

Lines changed: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import inspect
44
import typing as t
55
from dataclasses import dataclass, field
6+
from functools import cmp_to_key
67

78
from piccolo.apps.migrations.auto.diffable_table import DiffableTable
89
from piccolo.apps.migrations.auto.operations import (
@@ -117,6 +118,49 @@ def table_class_names(self) -> t.List[str]:
117118
return list(set([i.table_class_name for i in self.alter_columns]))
118119

119120

121+
def _compare_tables(
122+
table_a: t.Type[Table],
123+
table_b: t.Type[Table],
124+
iterations: int = 0,
125+
max_iterations=5,
126+
) -> int:
127+
"""
128+
A comparison function, for sorting Table classes, based on their foreign
129+
keys.
130+
131+
:param iterations:
132+
As this function is called recursively, we use this to limit the depth,
133+
to prevent an infinite loop.
134+
135+
"""
136+
if iterations >= max_iterations:
137+
return 0
138+
139+
for fk_column in table_a._meta.foreign_key_columns:
140+
references = fk_column._foreign_key_meta.resolved_references
141+
if references._meta.tablename == table_b._meta.tablename:
142+
return 1
143+
else:
144+
for _fk_column in references._meta.foreign_key_columns:
145+
_references = _fk_column._foreign_key_meta.resolved_references
146+
if _compare_tables(
147+
_references, table_b, iterations=iterations + 1
148+
):
149+
return 1
150+
151+
return -1
152+
153+
154+
def sort_table_classes(
155+
table_classes: t.List[t.Type[Table]],
156+
) -> t.List[t.Type[Table]]:
157+
"""
158+
Sort the table classes based on their foreign keys, so they can be created
159+
in the correct order.
160+
"""
161+
return sorted(table_classes, key=cmp_to_key(_compare_tables))
162+
163+
120164
@dataclass
121165
class MigrationManager:
122166
"""
@@ -563,25 +607,29 @@ async def _run_rename_columns(self, backwards=False):
563607
).run()
564608

565609
async def _run_add_tables(self, backwards=False):
610+
table_classes: t.List[t.Type[Table]] = []
611+
for add_table in self.add_tables:
612+
add_columns: t.List[
613+
AddColumnClass
614+
] = self.add_columns.for_table_class_name(add_table.class_name)
615+
_Table: t.Type[Table] = create_table_class(
616+
class_name=add_table.class_name,
617+
class_kwargs={"tablename": add_table.tablename},
618+
class_members={
619+
add_column.column._meta.name: add_column.column
620+
for add_column in add_columns
621+
},
622+
)
623+
table_classes.append(_Table)
624+
625+
# Sort by foreign key, so they're created in the right order.
626+
sorted_table_classes = sort_table_classes(table_classes)
627+
566628
if backwards:
567-
for add_table in self.add_tables:
568-
await add_table.to_table_class().alter().drop_table(
569-
cascade=True
570-
).run()
629+
for _Table in reversed(sorted_table_classes):
630+
await _Table.alter().drop_table(cascade=True).run()
571631
else:
572-
for add_table in self.add_tables:
573-
add_columns: t.List[
574-
AddColumnClass
575-
] = self.add_columns.for_table_class_name(add_table.class_name)
576-
_Table: t.Type[Table] = create_table_class(
577-
class_name=add_table.class_name,
578-
class_kwargs={"tablename": add_table.tablename},
579-
class_members={
580-
add_column.column._meta.name: add_column.column
581-
for add_column in add_columns
582-
},
583-
)
584-
632+
for _Table in sorted_table_classes:
585633
await _Table.create_table().run()
586634

587635
async def _run_add_columns(self, backwards=False):

tests/apps/migrations/auto/test_migration_manager.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,40 @@
11
import asyncio
2+
from unittest import TestCase
23
from unittest.mock import MagicMock, patch
34

4-
from piccolo.apps.migrations.auto import MigrationManager
5+
from piccolo.apps.migrations.auto.migration_manager import (
6+
MigrationManager,
7+
sort_table_classes,
8+
)
59
from piccolo.apps.migrations.commands.base import BaseMigrationManager
610
from piccolo.columns import Text, Varchar
711
from piccolo.columns.base import OnDelete, OnUpdate
812
from piccolo.columns.column_types import ForeignKey
913
from piccolo.conf.apps import AppConfig
1014
from piccolo.utils.lazy_loader import LazyLoader
1115
from tests.base import DBTestCase, postgres_only, set_mock_return_value
12-
from tests.example_app.tables import Manager
16+
from tests.example_app.tables import Band, Concert, Manager, Venue
1317

1418
asyncpg = LazyLoader("asyncpg", globals(), "asyncpg")
1519

1620

21+
class TestSortTableClasses(TestCase):
22+
def test_sort_table_classes(self):
23+
self.assertEqual(sort_table_classes([Manager, Band]), [Manager, Band])
24+
self.assertEqual(sort_table_classes([Band, Manager]), [Manager, Band])
25+
26+
sorted_tables = sort_table_classes([Manager, Venue, Concert, Band])
27+
self.assertTrue(
28+
sorted_tables.index(Manager) < sorted_tables.index(Band)
29+
)
30+
self.assertTrue(
31+
sorted_tables.index(Venue) < sorted_tables.index(Concert)
32+
)
33+
self.assertTrue(
34+
sorted_tables.index(Band) < sorted_tables.index(Concert)
35+
)
36+
37+
1738
class TestMigrationManager(DBTestCase):
1839
@postgres_only
1940
def test_rename_column(self):

tests/example_app/piccolo_migrations/2020-12-17T18-44-30.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ class Manager(Table, tablename="manager"):
1414
async def forwards():
1515
manager = MigrationManager(migration_id=ID, app_name="example_app")
1616

17-
manager.add_table("Manager", tablename="manager")
1817
manager.add_table("Band", tablename="band")
18+
manager.add_table("Manager", tablename="manager")
1919

2020
manager.add_column(
2121
table_class_name="Band",

0 commit comments

Comments
 (0)