Skip to content

Commit 89c46bd

Browse files
committed
Check migration outputs
1 parent 26adcc0 commit 89c46bd

File tree

13 files changed

+229
-5
lines changed

13 files changed

+229
-5
lines changed

.github/workflows/ci.yml

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
- uses: pre-commit/[email protected]
2626

2727
test:
28-
name: test ${{ matrix.py }}
28+
name: Test on Python ${{ matrix.py }}
2929
runs-on: ubuntu-latest
3030

3131
strategy:
@@ -63,3 +63,92 @@ jobs:
6363
run: python -m pip install tox-gh
6464
- name: Run test suite
6565
run: tox
66+
67+
migrations-tests:
68+
runs-on: ubuntu-latest
69+
services:
70+
postgres:
71+
image: postgres:15.2
72+
env:
73+
POSTGRES_USER: ${{ env.DB_USER }}
74+
POSTGRES_PASSWORD: ${{ env.DB_PASSWORD }}
75+
POSTGRES_DB: ${{ env.DB_NAME }}
76+
ports:
77+
- 5432:5432
78+
options: >-
79+
--health-cmd="pg_isready"
80+
--health-interval=10s
81+
--health-timeout=5s
82+
--health-retries=5
83+
strategy:
84+
fail-fast: false
85+
matrix:
86+
py:
87+
- "3.13"
88+
- "3.12"
89+
- "3.11"
90+
- "3.10"
91+
- "3.9"
92+
name: Check migrations on Python ${{ matrix.py }}
93+
94+
steps:
95+
- name: Checkout
96+
uses: actions/checkout@v4
97+
- name: Setup python for test ${{ matrix.py }}
98+
uses: actions/setup-python@v5
99+
with:
100+
python-version: ${{ matrix.py }}
101+
- name: Install Poetry
102+
uses: snok/install-poetry@v1
103+
- name: Install Deps
104+
run: |
105+
poetry install
106+
- name: Run Initial Migrations
107+
run: |
108+
poetry run python tests/manage.py migrate_schemas >initial_output.txt 2>&1
109+
echo "The output was"
110+
cat initial_output.txt
111+
echo "The expected output is"
112+
cat tests/expected_initial_output.txt
113+
echo "Diff:"
114+
diff initial_output.txt tests/expected_initial_output.txt
115+
- name: Create a dummy organisation
116+
run: |
117+
poetry run python tests/manage.py create_client test >create_output.txt 2>&1
118+
echo "The output was"
119+
cat create_output.txt
120+
echo "The expected output is"
121+
cat tests/expected_create_output.txt
122+
echo "Diff:"
123+
diff create_output.txt tests/expected_create_output.txt
124+
- name: Test migrate schemas (standard) if all migrated
125+
run: |
126+
poetry run python tests/manage.py migrate_schemas >standard_output.txt 2>&1
127+
echo "The output was"
128+
cat standard_output.txt
129+
echo "The expected output is"
130+
cat tests/expected_standard_output.txt
131+
echo "Diff:"
132+
diff standard_output.txt tests/expected_standard_output.txt
133+
134+
- name: Test migrate schemas (multiprocessing) if all migrated
135+
run: |
136+
poetry run python tests/manage.py migrate_schemas --executor multiprocessing >multiprocessing_output.txt 2>&1
137+
echo "The output was"
138+
cat multiprocessing_output.txt
139+
echo "The expected output is"
140+
cat tests/expected_multiprocessing_output.txt
141+
echo "Diff:"
142+
diff multiprocessing_output.txt tests/expected_multiprocessing_output.txt
143+
144+
- name: Upload migration test artifacts
145+
if: always()
146+
uses: actions/upload-artifact@v4
147+
with:
148+
name: migration-test-outputs-py${{ matrix.py }}
149+
path: |
150+
initial_output.txt
151+
create_output.txt
152+
standard_output.txt
153+
multiprocessing_output.txt
154+
retention-days: 7

__init__.py

Whitespace-only changes.

django_tenants_smart_executor/executors.py

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,43 @@
1010
import logging
1111
import multiprocessing
1212
from collections.abc import Iterable
13+
from contextlib import ContextDecorator
1314
from typing import Any
1415

1516
import django_tenants.migration_executors
1617
from django.conf import settings
17-
from django.db import connection
18+
from django.db import connection, connections
1819
from django.db.migrations.executor import MigrationExecutor
1920
from django.db.migrations.recorder import MigrationRecorder
2021
from django_tenants.migration_executors.base import run_migrations
2122
from django_tenants.signals import schema_migrated
22-
from django_tenants.utils import schema_context
23+
from django_tenants.utils import get_tenant_database_alias
2324

2425
logger = logging.getLogger("django_tenants_smart_executor")
2526

2627

28+
class schema_context_without_public(ContextDecorator): # noqa: N801
29+
"""
30+
Like schema_context, but without public schema.
31+
"""
32+
33+
def __init__(self, *args, **kwargs):
34+
self.schema_name = args[0]
35+
self.database = kwargs.get("database", get_tenant_database_alias())
36+
super().__init__()
37+
38+
def __enter__(self):
39+
self.connection = connections[self.database]
40+
self.previous_tenant = connection.tenant
41+
self.connection.set_schema(self.schema_name, include_public=False)
42+
43+
def __exit__(self, *exc):
44+
if self.previous_tenant is None:
45+
self.connection.set_schema_to_public()
46+
else:
47+
self.connection.set_tenant(self.previous_tenant)
48+
49+
2750
def needs_migrations(nodes: set[tuple[str, str]], schema_name: str, options: dict) -> bool:
2851
"""
2952
Returns whether we need to run migrations for a given schema.
@@ -35,8 +58,12 @@ def needs_migrations(nodes: set[tuple[str, str]], schema_name: str, options: dic
3558

3659
migrated_already: set[tuple[str, str]]
3760

38-
with schema_context(schema_name):
39-
migrated_already = set(MigrationRecorder(connection=connection).applied_migrations().keys())
61+
# need to exclude public schema so if there's no migration table it doesn't pick up the one in public
62+
with schema_context_without_public(schema_name):
63+
migration_recorder = MigrationRecorder(connection=connection)
64+
if not migration_recorder.has_table():
65+
return True
66+
migrated_already = set(migration_recorder.applied_migrations().keys())
4067

4168
for node in nodes:
4269
if node not in migrated_already:

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ ignore = [
8585
"RET507", # superfluous-else-continue
8686
"RET508", # superfluous-else-break
8787
"B027", # empty-method-without-abstract-decorator
88+
"N999", # invalid-module-name
8889
]
8990

9091
[tool.pytest.ini_options]

tests/expected_create_output.txt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
=== Starting migration
2+
Operations to perform:
3+
Apply all migrations: admin, auth, contenttypes, public_app, sessions, tenant_app
4+
Running migrations:
5+
Applying contenttypes.0001_initial...
6+
OK
7+
Applying auth.0001_initial...
8+
OK
9+
Applying admin.0001_initial...
10+
OK
11+
Applying admin.0002_logentry_remove_auto_add...
12+
OK
13+
Applying admin.0003_logentry_add_action_flag_choices...
14+
OK
15+
Applying contenttypes.0002_remove_content_type_name...
16+
OK
17+
Applying auth.0002_alter_permission_name_max_length...
18+
OK
19+
Applying auth.0003_alter_user_email_max_length...
20+
OK
21+
Applying auth.0004_alter_user_username_opts...
22+
OK
23+
Applying auth.0005_alter_user_last_login_null...
24+
OK
25+
Applying auth.0006_require_contenttypes_0002...
26+
OK
27+
Applying auth.0007_alter_validators_add_error_messages...
28+
OK
29+
Applying auth.0008_alter_user_username_max_length...
30+
OK
31+
Applying auth.0009_alter_user_last_name_max_length...
32+
OK
33+
Applying auth.0010_alter_group_name_max_length...
34+
OK
35+
Applying auth.0011_update_proxy_permissions...
36+
OK
37+
Applying auth.0012_alter_user_first_name_max_length...
38+
OK
39+
Applying public_app.0001_initial...
40+
OK
41+
Applying sessions.0001_initial...
42+
OK
43+
Applying tenant_app.0001_initial...
44+
OK

tests/expected_initial_output.txt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
=== Starting migration
2+
Operations to perform:
3+
Apply all migrations: admin, auth, contenttypes, public_app, sessions, tenant_app
4+
Running migrations:
5+
Applying contenttypes.0001_initial...
6+
OK
7+
Applying auth.0001_initial...
8+
OK
9+
Applying admin.0001_initial...
10+
OK
11+
Applying admin.0002_logentry_remove_auto_add...
12+
OK
13+
Applying admin.0003_logentry_add_action_flag_choices...
14+
OK
15+
Applying contenttypes.0002_remove_content_type_name...
16+
OK
17+
Applying auth.0002_alter_permission_name_max_length...
18+
OK
19+
Applying auth.0003_alter_user_email_max_length...
20+
OK
21+
Applying auth.0004_alter_user_username_opts...
22+
OK
23+
Applying auth.0005_alter_user_last_login_null...
24+
OK
25+
Applying auth.0006_require_contenttypes_0002...
26+
OK
27+
Applying auth.0007_alter_validators_add_error_messages...
28+
OK
29+
Applying auth.0008_alter_user_username_max_length...
30+
OK
31+
Applying auth.0009_alter_user_last_name_max_length...
32+
OK
33+
Applying auth.0010_alter_group_name_max_length...
34+
OK
35+
Applying auth.0011_update_proxy_permissions...
36+
OK
37+
Applying auth.0012_alter_user_first_name_max_length...
38+
OK
39+
Applying public_app.0001_initial...
40+
OK
41+
Applying sessions.0001_initial...
42+
OK
43+
Applying tenant_app.0001_initial...
44+
OK
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
No migrations needed for schema public, only triggering signals
2+
No migrations needed for schema test, only triggering signals

tests/expected_standard_output.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
No migrations needed for schema public, only triggering signals
2+
No migrations needed for schema test, only triggering signals

tests/manage.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
import os
55
import sys
6+
from pathlib import Path
67

78

89
def main():
@@ -20,4 +21,5 @@ def main():
2021

2122

2223
if __name__ == "__main__":
24+
sys.path.insert(0, str(Path(__file__).parent.parent.resolve()))
2325
main()

tests/test_project/public_app/management/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)