fix(db): explain why migration 0002 refuses to run - #180
Merged
Conversation
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
Merged
This was referenced Sep 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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:(conversation, member)pairs — JSONB member ids with noparticipants(id, company_id)row in the conversation's tenant, ignoringexternal:%markers;participantsentirely, id owned by another tenant, id that is ausersrow, id that is acompany_membersrow without a participant, conversations under thepersonaltenant, ids that authoredmessagesin that conversation;company_idseparately (0002'sSET NOT NULLwould trip on them first);23503with an actionable message.When everything resolves it logs one line and the SQL runs as before.
Verification
23503; reports NULL-tenant conversations even with zero orphans.pgvector/pgvector:pg16database seeded with a foreign participant, a vanished participant that authored a message, and a users-only member underpersonal:Ledger stays at version 1; after repairing the rows the rerun applies v2 (42 ms) and v3.
schema-migrations,migration-ownership,conversation-membership-source,migrate-client-releasesuites pass against Postgres; lint, typecheck, server:typecheck green.https://claude.ai/code/session_01SevbW9qCBbzrjfLMy14A31