Skip to content

fix: reconcile service_keys.identity_id FK to ON DELETE CASCADE on legacy DBs - #252

Open
safayavatsal wants to merge 1 commit into
highflame-ai:mainfrom
safayavatsal:fix/service-keys-fk-cascade-196
Open

fix: reconcile service_keys.identity_id FK to ON DELETE CASCADE on legacy DBs#252
safayavatsal wants to merge 1 commit into
highflame-ai:mainfrom
safayavatsal:fix/service-keys-fk-cascade-196

Conversation

@safayavatsal

Copy link
Copy Markdown
Contributor

Closes #196.

Summary

  • migrations/006_service_keys.up.sql declares service_keys.identity_id REFERENCES identities(id) ON DELETE CASCADE, but the migration uses CREATE TABLE IF NOT EXISTS, which is a no-op on any deployment where the table already existed before the cascade was added. Legacy DBs (dev1/stage1/prod lineage) kept a non-cascading FK; fresh DBs got the cascade. Declared schema vs. live state silently diverged.
  • That drift caused highflame-authn#109: hard-deleting an identity with a service key 500'd on legacy DBs (violates foreign key constraint "service_keys_identity_id_fkey"), breaking every agent delete from Studio's registry.
  • #187 fixed the user-visible symptom by switching the DELETE handlers to soft delete (DeactivateIdentity), sidestepping the FK entirely — the right immediate call, no risky migration, audit trail preserved.
  • This PR closes the latent schema bug the workaround left behind. The hard-delete path isn't gone: PurgeIdentity (the compensating rollback in AgentService.RegisterAgent) still does a real hard delete, and it only avoids tripping the FK today because it happens to run before any service-key row is persisted. That's fragile — a future reordering, or any other hard-delete caller (e.g. a GDPR-erasure path), would silently break again, but only on legacy DBs.

Changes

  • migrations/040_service_keys_fk_cascade.up.sql — drops and re-adds service_keys_identity_id_fkey with ON DELETE CASCADE. No-op on fresh DBs that already declare the cascade.
  • migrations/040_service_keys_fk_cascade.down.sql — reverts to ON DELETE NO ACTION.
  • tests/integration/service_keys_fk_cascade_test.go — registers an agent (auto-creates a bootstrap service key), then hard-deletes the identity directly at the repo layer (the same DELETE FROM identities ... that IdentityRepository.Delete / PurgeIdentity issues), and asserts the delete succeeds and the service key row cascades away.

Verification

Built a throwaway Postgres container replicating the legacy pre-migration-006 shape (service_keys.identity_id FK with no ON DELETE clause) and drove it end to end:

  • Before fix: reproduced the original bug verbatim — ERROR: update or delete on table "identities" violates foreign key constraint "service_keys_identity_id_fkey".
  • After applying 040...up.sql: pg_constraint.confdeltype flips from a (no action) to c (cascade); the same hard delete now succeeds and the referencing service_keys row is gone.
  • After applying 040...down.sql: confdeltype flips cleanly back to a — round-trip confirmed.

Also ran, all green:

  • GOEXPERIMENT=jsonv2 go build ./...
  • GOEXPERIMENT=jsonv2 go vet ./...
  • gofmt -l .
  • Full test suite (go test ./... -race -count=1 for unit packages; go test ./tests/integration/... for the testcontainers suite, including the new test and the existing TestDeleteAgent_WithServiceKey_SoftDeletes / TestRegisterAgent_ApiKeyPolicyBroaderThanIdentityRejected regression tests from fix: soft-delete identities/agents instead of hard delete (authn#109) #187)

Acceptance (from #196)

  • Migration drops + re-adds the FK with ON DELETE CASCADE — verified via pg_constraint.confdeltype on a simulated legacy DB (see Verification above). Please still confirm against a real dev1/stage1/prod snapshot before merge, per the issue's own caveat.
  • PurgeIdentity's underlying guarantee (a service-key-bearing identity's hard delete cascades) is exercised end-to-end in the new integration test.
  • No live request path regression — DELETE /agents/registry/{id} continues to soft-delete via DeactivateIdentity; existing regression tests for that path still pass unmodified.
  • Down migration round-trips cleanly (verified on the simulated legacy DB).

Locking / safety

As noted in the issue: both ALTER TABLE statements take ACCESS EXCLUSIVE on service_keys only (not identities) for the duration of the constraint flip. DROP CONSTRAINT is metadata-only; ADD CONSTRAINT does a full-table validation scan against identities, but service_keys is small (one row per issued API key), so this should be sub-second. Would still like migration-analyzer (or equivalent) to run before merge per the issue's request.

Out of scope

Per the issue: Studio UX consolidation and any user-facing hard-delete/GDPR-erasure path are tracked separately.

…gacy DBs

Migration 006 declares service_keys.identity_id REFERENCES
identities(id) ON DELETE CASCADE, but it uses CREATE TABLE IF NOT
EXISTS — a no-op on any deployment where the table already existed
before the cascade was added (dev1/stage1/prod lineage). Those
legacy DBs kept a non-cascading FK while fresh DBs got the cascade,
a silent declared-vs-actual schema drift.

That drift caused highflame-authn#109: hard-deleting an identity
with a service key 500'd on legacy DBs, breaking every agent delete
from Studio's registry. zeroid#187 fixed the user-visible symptom by
switching DELETE handlers to soft delete, sidestepping the FK
entirely. This closes the underlying drift the workaround left
behind: PurgeIdentity (the compensating rollback in
AgentService.RegisterAgent) still does a real hard delete, and only
avoids the FK today by the accident of running before any service
key row is persisted — a future reordering, or any hard-delete path
such as a GDPR-erasure feature, would silently break again on
legacy DBs only.

- migrations/040_service_keys_fk_cascade.{up,down}.sql: drop and
  re-add the FK with ON DELETE CASCADE (down flips to NO ACTION).
  Idempotent/no-op on fresh DBs that already declare the cascade.
- tests/integration/service_keys_fk_cascade_test.go: registers an
  agent (auto-creates a service key), hard-deletes the identity at
  the repo layer — the same DELETE FROM identities that
  IdentityRepository.Delete / PurgeIdentity issues — and asserts it
  succeeds and cascades the key row away.

Verified against a throwaway Postgres container built with the
legacy pre-cascade shape: reproduced the original FK violation
verbatim, confirmed the up migration flips pg_constraint.confdeltype
from 'a' to 'c' and the same hard delete then succeeds and cascades,
and confirmed the down migration round-trips back to 'a' cleanly.
Full test suite (unit + integration) green.

Closes highflame-ai#196
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

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.

bug: service_keys.identity_id FK declared CASCADE in migration 006 but legacy DBs carry non-cascading constraint

1 participant