// Slice B.1 (t-paliad-273) — migration 136 backfill invariants. // // The dry-run gate (migrate_test.go: TestMigrations_DryRun) catches // migrations that crash on apply, but it rolls back inside its own // transaction — the post-state assertions in mig 136's PL/pgSQL block // run, but a future refactor of those assertions might forget a check // or introduce a silent count drift. This test layers a Go-side // invariant check on top so the contract is restated in test code, // outside the PL/pgSQL block, against the resulting tables. // // Skipped without TEST_DATABASE_URL, same pattern as // internal/services/submission_codes_shape_test.go. package db import ( "context" "database/sql" "os" "testing" _ "github.com/lib/pq" ) // TestMigration136_BackfillInvariants applies every embedded migration // (which lands mig 136 along the way) and then asserts the four // invariants the B.1 design + B.0 findings nailed down: // // 1. procedural_events row count = (distinct submission_codes in // deadline_rules) + (deadline_rules with NULL submission_code). // Codes-bearing branch is 1:1 per the B.0 audit (no multi-row // codes since the _archived_litigation.* removal); the NULL // branch gets one synthetic procedural_event per rule. // 2. sequencing_rules row count = deadline_rules row count (1:1). // 3. legal_sources row count = distinct legal_source in // deadline_rules (NULL excluded). // 4. every sequencing_rules row's procedural_event_id resolves to a // procedural_events row (NOT NULL FK already enforces this at the // DB level — this test catches a future relaxation of the FK). // 5. no two synthetic codes collide (covered by the UNIQUE on // procedural_events.code; restated here for documentation). // // The test is robust against corpus size — it derives all expected // counts from the live deadline_rules state, so a scratch DB with 0 // rules trivially passes, and a prod-shaped scratch DB exercises the // real invariants. func TestMigration136_BackfillInvariants(t *testing.T) { url := os.Getenv("TEST_DATABASE_URL") if url == "" { t.Skip("TEST_DATABASE_URL not set — skipping mig 136 invariant test") } if err := ApplyMigrations(url); err != nil { t.Fatalf("apply migrations: %v", err) } conn, err := sql.Open("postgres", url) if err != nil { t.Fatalf("open: %v", err) } defer conn.Close() ctx := context.Background() var ( drTotal, drCodesDistinct, drCodesNull, drLegalDistinct int peTotal, srTotal, lsTotal int orphanPE, dupSynthetic int ) mustQ := func(label, q string, dst *int) { t.Helper() if err := conn.QueryRowContext(ctx, q).Scan(dst); err != nil { t.Fatalf("%s: %v", label, err) } } mustQ("dr_total", `SELECT COUNT(*) FROM paliad.deadline_rules`, &drTotal) mustQ("dr_codes_distinct", `SELECT COUNT(DISTINCT submission_code) FROM paliad.deadline_rules WHERE submission_code IS NOT NULL`, &drCodesDistinct) mustQ("dr_codes_null", `SELECT COUNT(*) FROM paliad.deadline_rules WHERE submission_code IS NULL`, &drCodesNull) mustQ("dr_legal_distinct", `SELECT COUNT(DISTINCT legal_source) FROM paliad.deadline_rules WHERE legal_source IS NOT NULL`, &drLegalDistinct) mustQ("pe_total", `SELECT COUNT(*) FROM paliad.procedural_events`, &peTotal) mustQ("sr_total", `SELECT COUNT(*) FROM paliad.sequencing_rules`, &srTotal) mustQ("ls_total", `SELECT COUNT(*) FROM paliad.legal_sources`, &lsTotal) // Invariant 1: procedural_events = distinct_codes + null_codes wantPE := drCodesDistinct + drCodesNull if peTotal != wantPE { t.Errorf("procedural_events count mismatch: got %d, want %d (distinct codes=%d + null-code rules=%d)", peTotal, wantPE, drCodesDistinct, drCodesNull) } // Invariant 2: sequencing_rules 1:1 with deadline_rules if srTotal != drTotal { t.Errorf("sequencing_rules count mismatch: got %d, want %d (1:1 with deadline_rules)", srTotal, drTotal) } // Invariant 3: legal_sources = distinct legal_source if lsTotal != drLegalDistinct { t.Errorf("legal_sources count mismatch: got %d, want %d (distinct legal_source)", lsTotal, drLegalDistinct) } // Invariant 4: every sequencing_rules.procedural_event_id resolves mustQ("orphan_pe", ` SELECT COUNT(*) FROM paliad.sequencing_rules sr LEFT JOIN paliad.procedural_events pe ON pe.id = sr.procedural_event_id WHERE pe.id IS NULL`, &orphanPE) if orphanPE != 0 { t.Errorf("FK integrity violated: %d sequencing_rules row(s) have no resolving procedural_event_id", orphanPE) } // Invariant 5: no duplicate synthetic codes mustQ("dup_synthetic", ` SELECT COUNT(*) FROM ( SELECT code FROM paliad.procedural_events WHERE code LIKE 'null.%' GROUP BY code HAVING COUNT(*) > 1 ) d`, &dupSynthetic) if dupSynthetic != 0 { t.Errorf("synthetic code uniqueness violated: %d duplicate(s) under 'null.%%' prefix", dupSynthetic) } t.Logf("mig 136 invariants OK: deadline_rules=%d, procedural_events=%d (=%d+%d), "+ "sequencing_rules=%d, legal_sources=%d (distinct legal_source=%d)", drTotal, peTotal, drCodesDistinct, drCodesNull, srTotal, lsTotal, drLegalDistinct) }