Skip to content

Commit b13c377

Browse files
Align collation-divergent public event IDs with the manifest (#181)
Align collation-divergent public event IDs with the manifest
2 parents 4227acb + b77989e commit b13c377

3 files changed

Lines changed: 99 additions & 1 deletion

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from __future__ import annotations
2+
3+
import json
4+
from pathlib import Path
5+
6+
from django.db import migrations
7+
8+
MANIFEST_PATH = Path(__file__).resolve().parents[1] / "event_identity_manifest.json"
9+
10+
11+
def align_public_ids_to_manifest(apps, schema_editor) -> None:
12+
del schema_editor
13+
event_model = apps.get_model("events", "Event")
14+
payload = json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
15+
manifest_ids = {item["id"]: item["public_id"] for item in payload["events"]}
16+
# The bootstrap backfill in 0006 orders by (source_key, id), which follows the database
17+
# collation: PostgreSQL sorted four hyphenated tie groups differently from the reviewed
18+
# manifest's byte-order numbering. Re-align the divergent rows to the manifest so every
19+
# environment serves the same canonical numeric paths.
20+
divergent = [
21+
event
22+
for event in event_model.objects.exclude(public_id=None).order_by("id")
23+
if str(event.id) in manifest_ids and event.public_id != manifest_ids[str(event.id)]
24+
]
25+
if not divergent:
26+
return
27+
# public_id is UNIQUE, so park every divergent row on a temporary offset above the real
28+
# range before writing the target values; the offset keeps each parked value distinct.
29+
# Queryset updates keep the immutable-runtime-ID save guard out of the historical path.
30+
offset = max(manifest_ids.values()) + len(manifest_ids)
31+
for event in divergent:
32+
event_model.objects.filter(pk=event.pk).update(public_id=event.public_id + offset)
33+
for event in divergent:
34+
event_model.objects.filter(pk=event.pk).update(public_id=manifest_ids[str(event.id)])
35+
36+
37+
class Migration(migrations.Migration):
38+
dependencies = [("events", "0007_reconcile_public_event_identity")]
39+
40+
operations = [
41+
migrations.RunPython(align_public_ids_to_manifest, migrations.RunPython.noop),
42+
]

events/tests/test_identity.py

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
from __future__ import annotations
22

3+
import importlib.util
34
import json
45
import re
56
import uuid
67
from copy import deepcopy
78
from pathlib import Path
8-
from typing import ClassVar
9+
from typing import Any, ClassVar
910
from unittest import mock
1011

1112
from django.core.exceptions import ImproperlyConfigured
@@ -30,6 +31,25 @@
3031
)
3132
from events.models import Event, EventAlias, EventPublicIdSequence
3233

34+
ROOT = Path(__file__).resolve().parents[2]
35+
36+
37+
def load_migration_function(relative_path: str, function_name: str) -> Any:
38+
module_spec = importlib.util.spec_from_file_location(
39+
f"loaded_{Path(relative_path).stem}", ROOT / relative_path
40+
)
41+
assert module_spec is not None and module_spec.loader is not None
42+
module = importlib.util.module_from_spec(module_spec)
43+
module_spec.loader.exec_module(module)
44+
return getattr(module, function_name)
45+
46+
47+
class _HistoricalAppsStub:
48+
def get_model(self, app_label: str, model_name: str) -> Any:
49+
if (app_label, model_name) != ("events", "Event"):
50+
raise AssertionError("unexpected historical model requested")
51+
return Event
52+
3353

3454
class EventIdentityManifestTests(TestCase):
3555
def test_checked_manifest_and_database_freeze_all_numeric_mappings_and_aliases(self) -> None:
@@ -111,6 +131,41 @@ def test_unavailable_database_serves_the_manifest_identity_snapshot(self) -> Non
111131
expected_paths,
112132
)
113133

134+
def test_collation_divergent_public_ids_realign_to_the_manifest(self) -> None:
135+
align = load_migration_function(
136+
"events/migrations/0008_align_public_event_ids_to_manifest.py",
137+
"align_public_ids_to_manifest",
138+
)
139+
manifest = load_identity_manifest()
140+
expected = {str(item.id): item.public_id for item in manifest.events}
141+
142+
first, second = manifest.events[11], manifest.events[12]
143+
Event.objects.filter(id=first.id).update(public_id=100_000)
144+
Event.objects.filter(id=second.id).update(public_id=first.public_id)
145+
Event.objects.filter(id=first.id).update(public_id=second.public_id)
146+
with self.assertRaisesMessage(
147+
ImproperlyConfigured,
148+
"Public Event UUID/public-ID mapping is incomplete",
149+
):
150+
public_projection()
151+
152+
align(_HistoricalAppsStub(), None)
153+
154+
self.assertEqual(
155+
dict(Event.objects.values_list("id", "public_id")),
156+
{uuid.UUID(key): value for key, value in expected.items()},
157+
)
158+
projection = public_projection()
159+
self.assertEqual(
160+
{event["public_path"] for event in projection["events"]},
161+
{item.canonical_path for item in manifest.events},
162+
)
163+
align(_HistoricalAppsStub(), None)
164+
self.assertEqual(
165+
dict(Event.objects.values_list("id", "public_id")),
166+
{uuid.UUID(key): value for key, value in expected.items()},
167+
)
168+
114169
def test_manifest_import_replay_is_byte_stable_and_a_preflight_noop(self) -> None:
115170
before = tuple(Event.objects.order_by("id").values_list("id", "public_id", "slug"))
116171
first = import_identity_manifest(dry_run=True)

test_support/tests/test_migrations.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ def test_every_data_migration_uses_historical_apps_without_runtime_side_effects(
7171
"events/migrations/0005_seed_event_identity_manifest.py",
7272
"events/migrations/0006_event_public_id.py",
7373
"events/migrations/0007_reconcile_public_event_identity.py",
74+
"events/migrations/0008_align_public_event_ids_to_manifest.py",
7475
},
7576
)
7677
for path in paths:

0 commit comments

Comments
 (0)