From 7f719169f164ba096c34aacd0968045930c5bd3e Mon Sep 17 00:00:00 2001 From: Heshan Wanigasooriya Date: Sat, 12 Sep 2026 23:04:54 -0400 Subject: [PATCH] fix (migration): update to use an atomic migrateion --- .../0002_inboxreply_draft_lifecycle.py | 6 +++ apps/inbox/tests/test_migration_0002.py | 53 ++++++++++++++++++- 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/apps/inbox/migrations/0002_inboxreply_draft_lifecycle.py b/apps/inbox/migrations/0002_inboxreply_draft_lifecycle.py index fd783258..dd26ad71 100644 --- a/apps/inbox/migrations/0002_inboxreply_draft_lifecycle.py +++ b/apps/inbox/migrations/0002_inboxreply_draft_lifecycle.py @@ -21,6 +21,12 @@ def _mark_existing_sent(apps, schema_editor): for reply in InboxReply.objects.exclude(sent_at=None).iterator(): InboxReply.objects.filter(pk=reply.pk).update(created_at=reply.sent_at) + if schema_editor.connection.vendor == "postgresql": + # AddField(db_index=True) queues CREATE INDEX until the schema editor + # exits. Drain the backfill's deferred FK checks before that DDL runs, + # keeping both the schema changes and backfill in one atomic migration. + schema_editor.execute("SET CONSTRAINTS ALL IMMEDIATE") + def _noop(apps, schema_editor): pass diff --git a/apps/inbox/tests/test_migration_0002.py b/apps/inbox/tests/test_migration_0002.py index ef9442b8..8ae79e7f 100644 --- a/apps/inbox/tests/test_migration_0002.py +++ b/apps/inbox/tests/test_migration_0002.py @@ -11,6 +11,8 @@ from datetime import timedelta import pytest +from django.db import connection +from django.db.migrations.executor import MigrationExecutor from django.utils import timezone from apps.inbox.models import InboxMessage, InboxReply @@ -58,8 +60,57 @@ def test_backfill_marks_existing_replies_sent(message): status=InboxReply.Status.DRAFT, sent_at=sent_at, created_at=timezone.now() ) - migration_module._mark_existing_sent(global_apps, None) + migration_module._mark_existing_sent(global_apps, connection.schema_editor()) reply.refresh_from_db() assert reply.status == InboxReply.Status.SENT assert reply.created_at == sent_at + + +@pytest.mark.django_db(transaction=True) +def test_upgrade_with_existing_replies_creates_status_index(message, user): + """Run the whole migration, including schema_editor's deferred CREATE INDEX. + + Calling only the backfill misses PostgreSQL's pending-trigger failure when + existing rows are updated before the new status index is created. + """ + before = [("inbox", "0001_initial")] + after = [("inbox", "0002_inboxreply_draft_lifecycle")] + executor = MigrationExecutor(connection) + latest = executor.loader.graph.leaf_nodes() + executor.migrate(before) + old_apps = executor.loader.project_state(before).apps + old_reply_model = old_apps.get_model("inbox", "InboxReply") + reply_ids = [] + sent_at = timezone.now() - timedelta(days=7) + try: + for number in range(2): + reply = old_reply_model.objects.create( + inbox_message_id=message.pk, + author_id=user.pk, + body=f"Already delivered {number}", + platform_reply_id=f"platform-reply-{number}", + ) + reply_ids.append(reply.pk) + old_reply_model.objects.filter(pk__in=reply_ids).update(sent_at=sent_at) + + MigrationExecutor(connection).migrate(after) + + for number, reply_id in enumerate(reply_ids): + reply = InboxReply.objects.get(pk=reply_id) + assert reply.status == InboxReply.Status.SENT + assert reply.created_at == reply.sent_at == sent_at + assert reply.body == f"Already delivered {number}" + assert reply.platform_reply_id == f"platform-reply-{number}" + assert reply.author_id == user.pk + with connection.cursor() as cursor: + indexes = connection.introspection.get_constraints(cursor, "inbox_reply") + assert any(index["index"] and index["columns"] == ["status"] for index in indexes.values()) + draft = InboxReply.objects.create(inbox_message=message, body="New draft") + assert draft.status == InboxReply.Status.DRAFT + assert draft.sent_at is None + draft.delete() + finally: + # Restore the schema for the rest of the suite. + old_reply_model.objects.filter(pk__in=reply_ids).delete() + MigrationExecutor(connection).migrate(latest)