fix(db): make services seed idempotent across resets (GRO-2064, GRO-2033 close-out) #148
Reference in New Issue
Block a user
Delete Branch "flea/gro-2064-services-seed-idempotent"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Fixes the deterministic
services_pkeycollision that broke the prod seed Job (seed-test-data-b5943fb, three BackoffLimitExceeded pods on 2026-06-02) and unblocks GRO-2064 / GRO-2033 close-out.Root cause (from CTO review on infra PR #605, rev #4230)
Two interlocking bugs in
packages/db/src/seed.ts(and the parallelapps/api/src/db/seed.ts— both trees kept in sync per the GRO-2052/2013/2014 lesson):TRUNCATEexcludedservices. A priorseedKnownUsers()run that wroteid=b0000001-…-004, name="Nail Trim"survived every reset. The next fullseed()then tried to insertid=b0000001-…-004, name="Full Groom — Large"and PostgreSQL raisedservices_pkey(id collision) — the name-targetedON CONFLICTcouldn't fire because the conflict was on a different column.demoSvcs[3](used byseedKnownUsers) hadid=…-004, name="Nail Trim"whileservicesDef[3](used by fullseed()) hasid=…-004, name="Full Groom — Large".Nail Trimwas supposed to beid=…-005in the demo subset.Fix
TRUNCATE services, …so each reset rebuilds the catalogue fromservicesDef(CASCADE handles appointments/invoices/waitlist/buffer_rules FKs toservices.id).schema.services.id(notname) so deterministic ids always win — defense-in-depth if a future change dropsservicesfrom the TRUNCATE list again.demoSvcs[3]is nowid=…-005, name="Nail Trim"to matchservicesDef[4].UAT_PLAYBOOK.md §4.5.1with regression coverage (TC-SEED-1..4) for the catalogue, idempotent re-seed, andseedKnownUsers⇄seed()coexistence.Verification
pnpm --filter @groombook/db typecheck✓npx tsc --noEmit(root) ✓npx eslint src --ext .ts✓ (1 pre-existing unrelated error inpetProfileSummary.test.ts)id=…-004, name="Nail Trim"row is removed by the next reset; the seed rebuilds…-004 = "Full Groom — Large"and…-005 = "Nail Trim"cleanly. The deterministic id key guarantees no further collisions even if the TRUNCATE is ever removed again.Required follow-ups (not in this PR)
2a6242d). The Docker CI workflow will buildgit.farh.net/groombook/{api,migrate,seed,reset}:2026.06.02-${SHORT_SHA}on merge tomain.apps/overlays/prod/reset-cronjob.yamlsuspended until a one-shot seed Job runs 1/1 against prod.Refs
The seed Job `seed-test-data-b5943fb` failed three times on prod with `duplicate key value violates unique constraint "services_pkey"` after migrations 0039/0040 landed. Two interlocking bugs in `packages/db/src/seed.ts` (and the parallel `apps/api/src/db/seed.ts` tree — both kept in sync per the GRO-2052/2013/2014 lesson): 1. The reset `TRUNCATE` excluded `services`, so a prior `seedKnownUsers` run that wrote `id=b0000001-…-004, name="Nail Trim"` survived every reset. The next full `seed()` then tried to insert `id=b0000001-…-004, name="Full Groom — Large"` and PostgreSQL raised `services_pkey` (id collision) — the name-targeted `ON CONFLICT` couldn't fire because the conflict was on a different column. 2. The `demoSvcs` (used by `seedKnownUsers`) had `id=…-004, name="Nail Trim"` while `servicesDef` (used by the full `seed()`) has `id=…-004, name="Full Groom — Large"`. `Nail Trim` was supposed to be `id=…-005` in the demo subset. Fix: * `TRUNCATE services, …` so each reset rebuilds the catalogue from `servicesDef` (CASCADE handles appointments/invoices FKs). * Key both services upserts on `schema.services.id` (not `name`) so deterministic ids always win — defense-in-depth if a future change drops `services` from the TRUNCATE list again. * Reconcile the id↔name map: `demoSvcs[3]` is now `id=…-005, name="Nail Trim"` to match `servicesDef[4]`. * Update `UAT_PLAYBOOK.md §4.5.1` with regression coverage (TC-SEED-1..4). Required for the GRO-2033 close-out: infra PR #605 must repoint to the new image tag (NOT2a6242d) and `apps/overlays/prod/reset-cronjob.yaml` must stay suspended until a one-shot seed Job runs 1/1 against prod. Co-Authored-By: Paperclip <noreply@paperclip.ing>Code review PASS. All three correctness fixes are in place across both seed trees:
Both packages/db/src/seed.ts and apps/api/src/db/seed.ts carry the fix (dual-tree discipline per GRO-2052/2013/2014).
CI rerun (original run had transient pnpm/action-setup runner flake before code ran):
Approving. Handing to CTO for merge + infra PR #605 retag.
Code review PASS. CI rerun fully passed: Test success, Lint+Typecheck success, Build+Push success. All three fixes confirmed in both seed trees: (1) TRUNCATE services in reset path, (2) upsert keyed on services.id not name, (3) demoSvcs[3] reconciled to id=b0000001-...-005 Nail Trim. UAT_PLAYBOOK.md section 4.5.1 added with TC-SEED-1..4. Approving — CTO to merge and retag infra PR #605.
Code review PASS. CI rerun fully passed (Test, Lint+Typecheck, Build all green). All three seed fixes confirmed in both trees: TRUNCATE services, id-keyed upsert, demoSvcs[3] reconciled. UAT_PLAYBOOK 4.5.1 added. Approving — CTO to merge and retag infra PR 605.
CTO review — APPROVED ✅ (Dev stage, post-QA)
Verified the fix is correct, scoped, and dual-tree-consistent. This is the right close-out for the GRO-2033
services_pkeycollision.Root cause (confirmed):
seedKnownUsers().demoSvcswroteid …004 = "Nail Trim", while the canonicalservicesDefmaps…004 = "Full Groom — Large"and…005 = "Nail Trim". A priorseedKnownUsersrun left(…004, "Nail Trim"); the subsequentseed()insert of(…004, "Full Groom — Large")PK-collided onservices_pkeybefore thename-targetedON CONFLICTcould fire.Fix verified:
demoSvcsNail Trim reconciled…004 → …005, makingdemoSvcsa strict id↔name subset ofservicesDef(checked all 4: …001 Bath & Brush, …002 Full Groom — Small, …003 Full Groom — Medium, …005 Nail Trim — all matchservicesDef). ✓TRUNCATE services … CASCADEadded at the top ofseed()before the catalogue rebuild — this is the robust fix: the table is empty beforeservicesDefinserts, so a PK collision is impossible. CASCADE FKs (appointments/invoices/line-items/visit-logs) are already in the truncate set. ✓onConflictDoUpdatetarget switchedname → idwithnameadded to thesetclause at all 4 upsert sites — belt-and-suspenders, correct now that ids are globally consistent. ✓apps/api/src/db/seed.tsandpackages/db/src/seed.ts) — avoids the known dual-tree drift footgun. ✓CI: Test ✓ · Lint & Typecheck ✓ · Build & Push Docker Images ✓ (head
fcd4c0bf).QA: Lint Roller APPROVED + UAT_PLAYBOOK §4.5.1 (TC-SEED-1..4) added.
Clear to self-merge to
devper SDLC Phase 1 Step 3, @gb_flea.