Skip to content

Commit 0fff088

Browse files
TheRealHaoLiuclaude
andcommitted
Fix Django 5.2 SQLite migration test failures in migrations 0144 and 0184
In Django 5.2+, SQLite table rewrites drop multi-column indexes, causing AlterIndexTogether and RenameIndex operations to fail when trying to modify indexes that no longer exist. Changes: - Add database-aware AlterIndexTogether and RenameIndex to _sqlite_helper.py - Update migration 0144 to use dbawaremigrations.AlterIndexTogether (5 operations) - Update migration 0184 to use dbawaremigrations.RenameIndex (18 operations) - Remove schema editor monkeypatch from settings_for_test.py - Remove AWX_MIGRATION_TESTS environment variable from Makefile The new operations check the database vendor and apply SQLite-specific error handling that ignores missing indexes, while using standard behavior for PostgreSQL. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent b412f43 commit 0fff088

File tree

5 files changed

+100
-78
lines changed

5 files changed

+100
-78
lines changed

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ test_coverage:
361361
fi
362362

363363
test_migrations:
364-
AWX_MIGRATION_TESTS=1 PYTHONDONTWRITEBYTECODE=1 py.test -p no:cacheprovider --migrations -m migration_test --create-db $(PARALLEL_TESTS) $(COVERAGE_ARGS) $(TEST_DIRS)
364+
PYTHONDONTWRITEBYTECODE=1 py.test -p no:cacheprovider --migrations -m migration_test --create-db $(PARALLEL_TESTS) $(COVERAGE_ARGS) $(TEST_DIRS)
365365
@if [ "${GITHUB_ACTIONS}" = "true" ]; \
366366
then \
367367
echo 'cov-report-files=reports/coverage.xml' >> "${GITHUB_OUTPUT}"; \

awx/main/migrations/0144_event_partitions.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -237,19 +237,19 @@ class Migration(migrations.Migration):
237237
db_index=False, editable=False, on_delete=models.deletion.DO_NOTHING, related_name='system_job_events', to='main.SystemJob'
238238
),
239239
),
240-
migrations.AlterIndexTogether(
240+
dbawaremigrations.AlterIndexTogether(
241241
name='adhoccommandevent',
242242
index_together={
243243
('ad_hoc_command', 'job_created', 'event'),
244244
('ad_hoc_command', 'job_created', 'counter'),
245245
('ad_hoc_command', 'job_created', 'uuid'),
246246
},
247247
),
248-
migrations.AlterIndexTogether(
248+
dbawaremigrations.AlterIndexTogether(
249249
name='inventoryupdateevent',
250250
index_together={('inventory_update', 'job_created', 'counter'), ('inventory_update', 'job_created', 'uuid')},
251251
),
252-
migrations.AlterIndexTogether(
252+
dbawaremigrations.AlterIndexTogether(
253253
name='jobevent',
254254
index_together={
255255
('job', 'job_created', 'counter'),
@@ -258,15 +258,15 @@ class Migration(migrations.Migration):
258258
('job', 'job_created', 'parent_uuid'),
259259
},
260260
),
261-
migrations.AlterIndexTogether(
261+
dbawaremigrations.AlterIndexTogether(
262262
name='projectupdateevent',
263263
index_together={
264264
('project_update', 'job_created', 'uuid'),
265265
('project_update', 'job_created', 'event'),
266266
('project_update', 'job_created', 'counter'),
267267
},
268268
),
269-
migrations.AlterIndexTogether(
269+
dbawaremigrations.AlterIndexTogether(
270270
name='systemjobevent',
271271
index_together={('system_job', 'job_created', 'uuid'), ('system_job', 'job_created', 'counter')},
272272
),

awx/main/migrations/0184_django_indexes.py

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
from django.db import migrations, models
77
import django.db.models.deletion
88

9+
from ._sqlite_helper import dbawaremigrations
10+
911

1012
class Migration(migrations.Migration):
1113
dependencies = [
@@ -15,92 +17,92 @@ class Migration(migrations.Migration):
1517
]
1618

1719
operations = [
18-
migrations.RenameIndex(
20+
dbawaremigrations.RenameIndex(
1921
model_name='adhoccommandevent',
2022
new_name='main_adhocc_ad_hoc__1e4d24_idx',
2123
old_fields=('ad_hoc_command', 'job_created', 'uuid'),
2224
),
23-
migrations.RenameIndex(
25+
dbawaremigrations.RenameIndex(
2426
model_name='adhoccommandevent',
2527
new_name='main_adhocc_ad_hoc__e72142_idx',
2628
old_fields=('ad_hoc_command', 'job_created', 'event'),
2729
),
28-
migrations.RenameIndex(
30+
dbawaremigrations.RenameIndex(
2931
model_name='adhoccommandevent',
3032
new_name='main_adhocc_ad_hoc__a57777_idx',
3133
old_fields=('ad_hoc_command', 'job_created', 'counter'),
3234
),
33-
migrations.RenameIndex(
35+
dbawaremigrations.RenameIndex(
3436
model_name='inventoryupdateevent',
3537
new_name='main_invent_invento_f72b21_idx',
3638
old_fields=('inventory_update', 'job_created', 'uuid'),
3739
),
38-
migrations.RenameIndex(
40+
dbawaremigrations.RenameIndex(
3941
model_name='inventoryupdateevent',
4042
new_name='main_invent_invento_364dcb_idx',
4143
old_fields=('inventory_update', 'job_created', 'counter'),
4244
),
43-
migrations.RenameIndex(
45+
dbawaremigrations.RenameIndex(
4446
model_name='jobevent',
4547
new_name='main_jobeve_job_id_40a56d_idx',
4648
old_fields=('job', 'job_created', 'parent_uuid'),
4749
),
48-
migrations.RenameIndex(
50+
dbawaremigrations.RenameIndex(
4951
model_name='jobevent',
5052
new_name='main_jobeve_job_id_3c4a4a_idx',
5153
old_fields=('job', 'job_created', 'uuid'),
5254
),
53-
migrations.RenameIndex(
55+
dbawaremigrations.RenameIndex(
5456
model_name='jobevent',
5557
new_name='main_jobeve_job_id_51c382_idx',
5658
old_fields=('job', 'job_created', 'counter'),
5759
),
58-
migrations.RenameIndex(
60+
dbawaremigrations.RenameIndex(
5961
model_name='jobevent',
6062
new_name='main_jobeve_job_id_0ddc6b_idx',
6163
old_fields=('job', 'job_created', 'event'),
6264
),
63-
migrations.RenameIndex(
65+
dbawaremigrations.RenameIndex(
6466
model_name='projectupdateevent',
6567
new_name='main_projec_project_449bbd_idx',
6668
old_fields=('project_update', 'job_created', 'uuid'),
6769
),
68-
migrations.RenameIndex(
70+
dbawaremigrations.RenameIndex(
6971
model_name='projectupdateevent',
7072
new_name='main_projec_project_69559a_idx',
7173
old_fields=('project_update', 'job_created', 'counter'),
7274
),
73-
migrations.RenameIndex(
75+
dbawaremigrations.RenameIndex(
7476
model_name='projectupdateevent',
7577
new_name='main_projec_project_c44b7c_idx',
7678
old_fields=('project_update', 'job_created', 'event'),
7779
),
78-
migrations.RenameIndex(
80+
dbawaremigrations.RenameIndex(
7981
model_name='role',
8082
new_name='main_rbac_r_content_979bdd_idx',
8183
old_fields=('content_type', 'object_id'),
8284
),
83-
migrations.RenameIndex(
85+
dbawaremigrations.RenameIndex(
8486
model_name='roleancestorentry',
8587
new_name='main_rbac_r_ancesto_b44606_idx',
8688
old_fields=('ancestor', 'content_type_id', 'role_field'),
8789
),
88-
migrations.RenameIndex(
90+
dbawaremigrations.RenameIndex(
8991
model_name='roleancestorentry',
9092
new_name='main_rbac_r_ancesto_22b9f0_idx',
9193
old_fields=('ancestor', 'content_type_id', 'object_id'),
9294
),
93-
migrations.RenameIndex(
95+
dbawaremigrations.RenameIndex(
9496
model_name='roleancestorentry',
9597
new_name='main_rbac_r_ancesto_c87b87_idx',
9698
old_fields=('ancestor', 'descendent'),
9799
),
98-
migrations.RenameIndex(
100+
dbawaremigrations.RenameIndex(
99101
model_name='systemjobevent',
100102
new_name='main_system_system__e39825_idx',
101103
old_fields=('system_job', 'job_created', 'uuid'),
102104
),
103-
migrations.RenameIndex(
105+
dbawaremigrations.RenameIndex(
104106
model_name='systemjobevent',
105107
new_name='main_system_system__73537a_idx',
106108
old_fields=('system_job', 'job_created', 'counter'),

awx/main/migrations/_sqlite_helper.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,78 @@
11
from django.db import migrations
22

33

4+
class AlterIndexTogether(migrations.AlterIndexTogether):
5+
"""
6+
Database-aware AlterIndexTogether that handles SQLite's missing indexes gracefully.
7+
8+
In Django 5.2+, SQLite table rewrites (triggered by AlterField operations)
9+
can drop multi-column indexes. For SQLite, this catches the ValueError and
10+
ignores it when the index doesn't exist. For PostgreSQL, uses standard behavior.
11+
"""
12+
13+
def database_forwards(self, app_label, schema_editor, from_state, to_state):
14+
if not schema_editor.connection.vendor.startswith('postgres'):
15+
# SQLite-specific handling: ignore missing indexes from table rewrites
16+
try:
17+
super().database_forwards(app_label, schema_editor, from_state, to_state)
18+
except ValueError as exc:
19+
if "Found wrong number (0) of constraints" in str(exc) or "Found wrong number (0) of indexes" in str(exc):
20+
return
21+
raise
22+
else:
23+
# PostgreSQL: standard behavior
24+
super().database_forwards(app_label, schema_editor, from_state, to_state)
25+
26+
def database_backwards(self, app_label, schema_editor, from_state, to_state):
27+
if not schema_editor.connection.vendor.startswith('postgres'):
28+
# SQLite-specific handling: ignore missing indexes from table rewrites
29+
try:
30+
super().database_backwards(app_label, schema_editor, from_state, to_state)
31+
except ValueError as exc:
32+
if "Found wrong number (0) of constraints" in str(exc) or "Found wrong number (0) of indexes" in str(exc):
33+
return
34+
raise
35+
else:
36+
# PostgreSQL: standard behavior
37+
super().database_backwards(app_label, schema_editor, from_state, to_state)
38+
39+
40+
class RenameIndex(migrations.RenameIndex):
41+
"""
42+
Database-aware RenameIndex that handles SQLite's missing indexes gracefully.
43+
44+
In Django 5.2+, SQLite table rewrites (triggered by AlterField operations)
45+
can drop multi-column indexes. For SQLite, this catches the ValueError and
46+
ignores it when the index doesn't exist. For PostgreSQL, uses standard behavior.
47+
"""
48+
49+
def database_forwards(self, app_label, schema_editor, from_state, to_state):
50+
if not schema_editor.connection.vendor.startswith('postgres'):
51+
# SQLite-specific handling: ignore missing indexes from table rewrites
52+
try:
53+
super().database_forwards(app_label, schema_editor, from_state, to_state)
54+
except ValueError as exc:
55+
if "Found wrong number (0) of constraints" in str(exc) or "wrong number (0) of indexes" in str(exc):
56+
return
57+
raise
58+
else:
59+
# PostgreSQL: standard behavior
60+
super().database_forwards(app_label, schema_editor, from_state, to_state)
61+
62+
def database_backwards(self, app_label, schema_editor, from_state, to_state):
63+
if not schema_editor.connection.vendor.startswith('postgres'):
64+
# SQLite-specific handling: ignore missing indexes from table rewrites
65+
try:
66+
super().database_backwards(app_label, schema_editor, from_state, to_state)
67+
except ValueError as exc:
68+
if "Found wrong number (0) of constraints" in str(exc) or "wrong number (0) of indexes" in str(exc):
69+
return
70+
raise
71+
else:
72+
# PostgreSQL: standard behavior
73+
super().database_backwards(app_label, schema_editor, from_state, to_state)
74+
75+
476
class RunSQL(migrations.operations.special.RunSQL):
577
"""
678
Bit of a hack here. Django actually wants this decision made in the router
@@ -56,6 +128,8 @@ def database_backwards(self, app_label, schema_editor, from_state, to_state):
56128
class _sqlitemigrations:
57129
RunPython = RunPython
58130
RunSQL = RunSQL
131+
AlterIndexTogether = AlterIndexTogether
132+
RenameIndex = RenameIndex
59133

60134

61135
dbawaremigrations = _sqlitemigrations()

awx/main/tests/settings_for_test.py

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -24,57 +24,3 @@
2424
},
2525
}
2626
}
27-
28-
# SQLite drops multi-column indexes when it rebuilds tables during some schema
29-
# operations, so later index-altering steps can fail while trying to remove
30-
# indexes that no longer exist. Limit the monkeypatch to migration smoke tests
31-
# (the `-m migration_test` target) so normal test runs remain untouched.
32-
if DATABASES['default']['ENGINE'] == 'django.db.backends.sqlite3':
33-
import os
34-
35-
if os.environ.get('AWX_MIGRATION_TESTS'):
36-
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
37-
from django.db.backends.sqlite3.schema import DatabaseSchemaEditor as SQLiteSchemaEditor
38-
from django.db.migrations.operations import models as migration_operations
39-
40-
_orig_delete_composed_index = SQLiteSchemaEditor._delete_composed_index
41-
_orig_base_delete_composed_index = BaseDatabaseSchemaEditor._delete_composed_index
42-
43-
def _safe_delete_composed_index(self, model, fields, *args, **kwargs):
44-
try:
45-
return _orig_delete_composed_index(self, model, fields, *args, **kwargs)
46-
except ValueError as exc:
47-
if self.connection.vendor == 'sqlite' and (
48-
"Found wrong number (0) of constraints" in str(exc) or "Found wrong number (0) of indexes" in str(exc)
49-
):
50-
return
51-
raise
52-
53-
SQLiteSchemaEditor._delete_composed_index = _safe_delete_composed_index
54-
55-
def _safe_base_delete_composed_index(self, model, fields, *args, **kwargs):
56-
try:
57-
return _orig_base_delete_composed_index(self, model, fields, *args, **kwargs)
58-
except ValueError as exc:
59-
if self.connection.vendor == 'sqlite' and (
60-
"Found wrong number (0) of constraints" in str(exc) or "Found wrong number (0) of indexes" in str(exc)
61-
):
62-
return
63-
raise
64-
65-
BaseDatabaseSchemaEditor._delete_composed_index = _safe_base_delete_composed_index
66-
67-
_orig_rename_index_forwards = migration_operations.RenameIndex.database_forwards
68-
69-
def _safe_rename_index_forwards(self, app_label, schema_editor, from_state, to_state):
70-
try:
71-
return _orig_rename_index_forwards(self, app_label, schema_editor, from_state, to_state)
72-
except ValueError as exc:
73-
# SQLite may have already dropped the index when rewriting the table.
74-
if schema_editor.connection.vendor == 'sqlite' and (
75-
"Found wrong number (0) of constraints" in str(exc) or "wrong number (0) of indexes" in str(exc)
76-
):
77-
return
78-
raise
79-
80-
migration_operations.RenameIndex.database_forwards = _safe_rename_index_forwards

0 commit comments

Comments
 (0)