fix: reconcile service_keys.identity_id FK to ON DELETE CASCADE on legacy DBs - #252
Open
safayavatsal wants to merge 1 commit into
Open
fix: reconcile service_keys.identity_id FK to ON DELETE CASCADE on legacy DBs#252safayavatsal wants to merge 1 commit into
safayavatsal wants to merge 1 commit into
Conversation
…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
Contributor
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
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.
Closes #196.
Summary
migrations/006_service_keys.up.sqldeclaresservice_keys.identity_id REFERENCES identities(id) ON DELETE CASCADE, but the migration usesCREATE 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.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.#187fixed 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.PurgeIdentity(the compensating rollback inAgentService.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-addsservice_keys_identity_id_fkeywithON DELETE CASCADE. No-op on fresh DBs that already declare the cascade.migrations/040_service_keys_fk_cascade.down.sql— reverts toON 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 sameDELETE FROM identities ...thatIdentityRepository.Delete/PurgeIdentityissues), 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_idFK with noON DELETEclause) and drove it end to end:ERROR: update or delete on table "identities" violates foreign key constraint "service_keys_identity_id_fkey".040...up.sql:pg_constraint.confdeltypeflips froma(no action) toc(cascade); the same hard delete now succeeds and the referencingservice_keysrow is gone.040...down.sql:confdeltypeflips cleanly back toa— round-trip confirmed.Also ran, all green:
GOEXPERIMENT=jsonv2 go build ./...GOEXPERIMENT=jsonv2 go vet ./...gofmt -l .go test ./... -race -count=1for unit packages;go test ./tests/integration/...for the testcontainers suite, including the new test and the existingTestDeleteAgent_WithServiceKey_SoftDeletes/TestRegisterAgent_ApiKeyPolicyBroaderThanIdentityRejectedregression tests from fix: soft-delete identities/agents instead of hard delete (authn#109) #187)Acceptance (from #196)
ON DELETE CASCADE— verified viapg_constraint.confdeltypeon 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.DELETE /agents/registry/{id}continues to soft-delete viaDeactivateIdentity; existing regression tests for that path still pass unmodified.Locking / safety
As noted in the issue: both
ALTER TABLEstatements takeACCESS EXCLUSIVEonservice_keysonly (notidentities) for the duration of the constraint flip.DROP CONSTRAINTis metadata-only;ADD CONSTRAINTdoes a full-table validation scan againstidentities, butservice_keysis small (one row per issued API key), so this should be sub-second. Would still likemigration-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.