Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/inbox/migrations/0002_inboxreply_draft_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
53 changes: 52 additions & 1 deletion apps/inbox/tests/test_migration_0002.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Loading