Skip to content

Commit e27cff6

Browse files
Fixed #36652 -- Increased determinism when loading migrations from disk.
Ordering still depends on pkgutil.iter_modules, which does not guarantee order, but at least now Django is not introducing additional indeterminism, causing CircularDependencyError to appear or not appear in some edge cases. Co-authored-by: Jacob Walls <jacobtylerwalls@gmail.com>
1 parent eaf7b56 commit e27cff6

10 files changed

Lines changed: 133 additions & 2 deletions

File tree

django/db/migrations/loader.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,11 @@ def load_disk(self):
109109
if was_loaded:
110110
reload(module)
111111
self.migrated_apps.add(app_config.label)
112-
migration_names = {
112+
migration_names = [
113113
name
114114
for _, name, is_pkg in pkgutil.iter_modules(module.__path__)
115115
if not is_pkg and name[0] not in "_~"
116-
}
116+
]
117117
# Load migrations
118118
for migration_name in migration_names:
119119
migration_path = "%s.%s" % (module_name, migration_name)

tests/migrations/test_loader.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import compileall
22
import os
3+
import subprocess
4+
import sys
5+
import tempfile
36
from importlib import import_module
7+
from pathlib import Path
48

9+
from django.conf import settings
510
from django.db import connection, connections
611
from django.db.migrations.exceptions import (
712
AmbiguityError,
@@ -649,6 +654,70 @@ def test_loading_package_without__file__(self):
649654
test_module.__spec__.origin = module_origin
650655
test_module.__spec__.has_location = module_has_location
651656

657+
def test_loading_order_does_not_create_circular_dependency(self):
658+
"""
659+
Before, for these migrations:
660+
app1
661+
[ ] 0001_squashed_initial <- replaces app1.0001
662+
[ ] 0002_squashed_initial <- replaces app1.0001
663+
depends on app1.0001_squashed_initial & app2.0001_squashed_initial
664+
app2
665+
[ ] 0001_squashed_initial <- replaces app2.0001
666+
667+
When loading app1's migrations, if 0002_squashed_initial was first:
668+
{'0002_squashed_initial', '0001_initial', '0001_squashed_initial'}
669+
Then CircularDependencyError was raised, but it's resolvable as:
670+
{'0001_initial', '0001_squashed_initial', '0002_squashed_initial'}
671+
"""
672+
# Create a test settings file to provide to the subprocess.
673+
MIGRATION_MODULES = {
674+
"app1": "migrations.test_migrations_squashed_replaced_order.app1",
675+
"app2": "migrations.test_migrations_squashed_replaced_order.app2",
676+
}
677+
INSTALLED_APPS = [
678+
"migrations.test_migrations_squashed_replaced_order.app1",
679+
"migrations.test_migrations_squashed_replaced_order.app2",
680+
]
681+
tests_dir = Path(__file__).parent.parent
682+
with tempfile.NamedTemporaryFile(
683+
mode="w", encoding="utf-8", suffix=".py", dir=tests_dir, delete=False
684+
) as test_settings:
685+
for attr, value in settings._wrapped.__dict__.items():
686+
if attr.isupper():
687+
test_settings.write(f"{attr} = {value!r}\n")
688+
# Provide overrides here, instead of via decorators.
689+
test_settings.write(f"DATABASES = {settings.DATABASES}\n")
690+
test_settings.write(f"MIGRATION_MODULES = {MIGRATION_MODULES}\n")
691+
# Isolate away other test apps.
692+
test_settings.write(
693+
"INSTALLED_APPS=[a for a in INSTALLED_APPS if a.startswith('django')]\n"
694+
)
695+
test_settings.write(f"INSTALLED_APPS += {INSTALLED_APPS}\n")
696+
test_settings_name = test_settings.name
697+
self.addCleanup(os.remove, test_settings_name)
698+
699+
test_environ = os.environ.copy()
700+
test_environ["PYTHONPATH"] = str(tests_dir)
701+
# Ensure deterministic failures.
702+
test_environ["PYTHONHASHSEED"] = "1"
703+
704+
args = [
705+
sys.executable,
706+
"-m",
707+
"django",
708+
"showmigrations",
709+
"app1",
710+
"--skip-checks",
711+
"--settings",
712+
Path(test_settings_name).stem,
713+
]
714+
try:
715+
subprocess.run(
716+
args, capture_output=True, env=test_environ, check=True, text=True
717+
)
718+
except subprocess.CalledProcessError as err:
719+
self.fail(err.stderr)
720+
652721

653722
class PycLoaderTests(MigrationTestBase):
654723
def test_valid(self):

tests/migrations/test_migrations_squashed_replaced_order/__init__.py

Whitespace-only changes.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from django.db import migrations
2+
3+
4+
class Migration(migrations.Migration):
5+
initial = True
6+
7+
dependencies = []
8+
9+
operations = []
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from django.db import migrations
2+
3+
4+
class Migration(migrations.Migration):
5+
initial = True
6+
7+
replaces = [
8+
("app1", "0001_initial"),
9+
]
10+
11+
dependencies = []
12+
13+
operations = []
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from django.db import migrations
2+
3+
4+
class Migration(migrations.Migration):
5+
initial = True
6+
7+
replaces = [
8+
("app1", "0001_initial"),
9+
]
10+
11+
dependencies = [
12+
("app1", "0001_squashed_initial"),
13+
("app2", "0001_squashed_initial"),
14+
]
15+
16+
operations = []

tests/migrations/test_migrations_squashed_replaced_order/app1/__init__.py

Whitespace-only changes.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from django.db import migrations
2+
3+
4+
class Migration(migrations.Migration):
5+
initial = True
6+
7+
dependencies = [
8+
("app1", "0001_initial"),
9+
]
10+
11+
operations = []
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from django.db import migrations
2+
3+
4+
class Migration(migrations.Migration):
5+
initial = True
6+
7+
replaces = [
8+
("app2", "0001_initial"),
9+
]
10+
11+
dependencies = []
12+
13+
operations = []

tests/migrations/test_migrations_squashed_replaced_order/app2/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)