Skip to content

fix(db): explain why migration 0002 refuses to run - #180

Merged
yetone merged 1 commit into
mainfrom
fix/migration-0002-precheck
Sep 3, 2026
Merged

fix(db): explain why migration 0002 refuses to run#180
yetone merged 1 commit into
mainfrom
fix/migration-0002-precheck

Conversation

@yetone

@yetone yetone commented Sep 3, 2026

Copy link
Copy Markdown
Owner

Why

The v0.14.1 deploy (run 33746384310) applied the frozen baseline to production (49 s, ledger now at version 1) and then stopped in migration 0002 with only:

[migrate-bin] failed: error: cannot normalize conversation membership: foreign or missing participant

Failing closed there is correct (#168, ADR 0004), but the bare 23503 tells an operator nothing about what the data looks like or how much of it there is, and the pre-deploy Job is the only place we get to look at production (deploy.yml, ADR 0003).

What

A read-only precheck in applyNormalizedConversationMembers, run on the same connection before the immutable 0002 SQL, so the v2 checksum stays untouched (no history edit). It:

  • counts the unresolvable (conversation, member) pairs — JSONB member ids with no participants(id, company_id) row in the conversation's tenant, ignoring external:% markers;
  • classifies them: id gone from participants entirely, id owned by another tenant, id that is a users row, id that is a company_members row without a participant, conversations under the personal tenant, ids that authored messages in that conversation;
  • reports conversations with a NULL company_id separately (0002's SET NOT NULL would trip on them first);
  • prints ≤15 sample rows with email-like ids masked, then throws the same 23503 with an actionable message.

When everything resolves it logs one line and the SQL runs as before.

Verification

  • Unit tests (fake client): passes and issues a single read-only statement when clean; fails closed with counts, the sample, masking, and code 23503; reports NULL-tenant conversations even with zero orphans.
  • v0.13.2-shaped pgvector/pgvector:pg16 database seeded with a foreign participant, a vanished participant that authored a message, and a users-only member under personal:
[db] applying migration 2 0002_normalized_conversation_members
[db] migration 0002 precheck failed: pairs=3 conversations=2 member_ids=3 missing_everywhere=2 other_tenant=1 user_rows=1 company_members_without_participant=1 personal_tenant=1 authored_messages=1 null_company_conversations=0
[db]   conversation=c-bad1 company=acme member=p-foreign [participant-in-other-tenant]
[db]   conversation=c-bad1 company=acme member=p-gone [no-participant-anywhere,authored-messages-here]
[db]   conversation=c-bad2 company=personal member=u-1 [no-participant-anywhere,users-row]
[migrate-bin] failed: Error: migration 0002 precondition failed: 3 conversation member id(s) in 2 conversation(s) do not resolve to a same-tenant participant — repair the data, then rerun the migration Job (see the precheck lines above and ADR 0004)

Ledger stays at version 1; after repairing the rows the rerun applies v2 (42 ms) and v3.

  • Fresh database adopts v1→v3 unchanged; schema-migrations, migration-ownership, conversation-membership-source, migrate-client-release suites pass against Postgres; lint, typecheck, server:typecheck green.

https://claude.ai/code/session_01SevbW9qCBbzrjfLMy14A31

The v0.14.1 adoption Job on production applied the frozen baseline and
then stopped in migration 0002 with only "cannot normalize conversation
membership: foreign or missing participant" (23503). That is the right
place to fail closed, but the message gives an operator without a psql
session nothing to act on.

Add a read-only precheck that runs on the same connection before the
immutable 0002 SQL (the v2 checksum is untouched): it counts the
unresolvable (conversation, member) pairs, classifies them — id gone from
participants entirely, id owned by another tenant, id that is a users row
or a company_members row without a participant, conversations under the
'personal' tenant, ids that authored messages in that conversation — and
prints a bounded sample with email-like ids masked, then raises the same
23503 with an actionable message. Conversations with a NULL company_id
are reported separately since the migration's SET NOT NULL would trip on
them first.

Verified against a v0.13.2-shaped pgvector database seeded with a foreign
participant, a vanished participant that authored a message, and a
users-only member under 'personal': the Job stops at the ledger's version
1 with the categorized report; after repairing the rows the rerun applies
v2 and v3. Fresh databases still adopt v1→v3 unchanged.

Claude-Session: https://claude.ai/code/session_01SevbW9qCBbzrjfLMy14A31
@yetone
yetone merged commit 960c271 into main Sep 3, 2026
7 checks passed
@yetone yetone mentioned this pull request Sep 3, 2026
@yetone
yetone deleted the fix/migration-0002-precheck branch September 3, 2026 11:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant