Add alembic migration for ondelete=CASCADE on organization child FKs (fixes #96) #103

Closed
Mark wants to merge 0 commits from fix/96-org-fk-cascade into main
Owner

Closes #96.

What

d0723c9 added ondelete="CASCADE" to the org_id foreign keys on
OrganizationEmail, OrganizationPhone, and OrganizationDomain in
app/models.py, but no alembic migration was generated — existing
databases keep the old FKs (NO ACTION), so deleting an organization in
production raises a foreign-key violation on child rows instead of
cascading.

This PR adds one migration (c9e3721d770e) that drops and recreates the
three FKs with ON DELETE CASCADE:

table constraint referenced
organization_email organization_email_org_id_fkey organization(id)
organization_phone organization_phone_org_id_fkey organization(id)
organization_domain organization_domain_org_id_fkey organization(id)

Constraint names were taken from the inspection task (SQLModel create_all
emits these FKs unnamed, so Postgres auto-names them
<table>_<column>_fkey) and re-verified on scratch Postgres 16 before
upgrade.

app/models.py was not touched; no other migration was modified.

Sanity checks (scratch Postgres 16, mirrors create_all schema path)

1. alembic upgrade head against a fresh DB — applies cleanly

INFO  [alembic.runtime.migration] Running upgrade a25b7f06b9f1 -> c9e3721d770e, cascade delete on organization child FKs

2. alembic upgrade head against a DB pre-seeded with orgs + child rows — applies cleanly

Same single migration applied; org 9001 seeded with 2 emails, 1 phone, 1 domain.

3. Manual DELETE verification (psql output)

Before delete:

 emails | phones | domains
--------+--------+---------
      2 |      1 |       1
(1 row)

DELETE FROM organization WHERE id=9001;:

DELETE 1

After delete — child rows removed, no FK violation:

 emails | phones | domains
--------+--------+---------
      0 |      0 |       0
(1 row)

 org_row_still_exists
----------------------
                    0
(1 row)

4. alembic downgrade -1 — restores previous FKs without ondelete

 organization_domain_org_id_fkey | FOREIGN KEY (org_id) REFERENCES organization(id)
 organization_email_org_id_fkey  | FOREIGN KEY (org_id) REFERENCES organization(id)
 organization_phone_org_id_fkey  | FOREIGN KEY (org_id) REFERENCES organization(id)

Note (pre-existing, not introduced here)

alembic upgrade head on a truly empty database fails at the root
migration 8b74cf400a42 (relation "case" does not exist) because the
base tables (case, user, organization, …) are owned by
SQLModel.metadata.create_all (app boot), not by any migration. Verified
identical failure with and without this PR's migration. Fresh deployments
bootstrap via create_all (entrypoint.sh) and then migrations apply on
top; that is the path this migration was verified against.

Closes #96. ## What `d0723c9` added `ondelete="CASCADE"` to the `org_id` foreign keys on `OrganizationEmail`, `OrganizationPhone`, and `OrganizationDomain` in `app/models.py`, but no alembic migration was generated — existing databases keep the old FKs (NO ACTION), so deleting an organization in production raises a foreign-key violation on child rows instead of cascading. This PR adds one migration (`c9e3721d770e`) that drops and recreates the three FKs with `ON DELETE CASCADE`: | table | constraint | referenced | |---|---|---| | organization_email | `organization_email_org_id_fkey` | organization(id) | | organization_phone | `organization_phone_org_id_fkey` | organization(id) | | organization_domain | `organization_domain_org_id_fkey` | organization(id) | Constraint names were taken from the inspection task (SQLModel create_all emits these FKs unnamed, so Postgres auto-names them `<table>_<column>_fkey`) and re-verified on scratch Postgres 16 before upgrade. `app/models.py` was not touched; no other migration was modified. ## Sanity checks (scratch Postgres 16, mirrors create_all schema path) ### 1. `alembic upgrade head` against a fresh DB — applies cleanly ``` INFO [alembic.runtime.migration] Running upgrade a25b7f06b9f1 -> c9e3721d770e, cascade delete on organization child FKs ``` ### 2. `alembic upgrade head` against a DB pre-seeded with orgs + child rows — applies cleanly Same single migration applied; org 9001 seeded with 2 emails, 1 phone, 1 domain. ### 3. Manual DELETE verification (psql output) Before delete: ``` emails | phones | domains --------+--------+--------- 2 | 1 | 1 (1 row) ``` `DELETE FROM organization WHERE id=9001;`: ``` DELETE 1 ``` After delete — child rows removed, no FK violation: ``` emails | phones | domains --------+--------+--------- 0 | 0 | 0 (1 row) org_row_still_exists ---------------------- 0 (1 row) ``` ### 4. `alembic downgrade -1` — restores previous FKs without ondelete ``` organization_domain_org_id_fkey | FOREIGN KEY (org_id) REFERENCES organization(id) organization_email_org_id_fkey | FOREIGN KEY (org_id) REFERENCES organization(id) organization_phone_org_id_fkey | FOREIGN KEY (org_id) REFERENCES organization(id) ``` ## Note (pre-existing, not introduced here) `alembic upgrade head` on a truly *empty* database fails at the root migration `8b74cf400a42` (`relation "case" does not exist`) because the base tables (`case`, `user`, `organization`, …) are owned by `SQLModel.metadata.create_all` (app boot), not by any migration. Verified identical failure with and without this PR's migration. Fresh deployments bootstrap via `create_all` (entrypoint.sh) and then migrations apply on top; that is the path this migration was verified against.
Add alembic migration for ondelete=CASCADE on organization child FKs (fixes #96)
Some checks failed
CI / Backend Lint & Format (pull_request) Failing after 4s
CI / Golden Fixture Validation (pull_request) Has been skipped
CI / Backend Tests (pull_request) Has been skipped
CI / Backend Security Tests (pull_request) Has been skipped
CI / Frontend Lint (pull_request) Failing after 22s
CI / Frontend Tests (pull_request) Has been skipped
CI / Dependency Security Scan (pull_request) Has been skipped
Stress Tests / Stress & Pipeline Tests (pull_request) Failing after 6s
Stress Tests / Full Data Pipeline Validation (pull_request) Has been skipped
Performance Regression Tests / Performance Regression Detection (pull_request) Failing after 33s
CI / Frontend E2E Tests (pull_request) Has been skipped
Performance Regression Tests / Compare with Baseline (pull_request) Has been skipped
CI / All Checks Passed (pull_request) Failing after 49s
c91c21963d
Drop and recreate the organization_email/phone/domain org_id foreign keys
with ON DELETE CASCADE so deleting an organization cascades to its child
rows instead of raising a foreign-key violation.

Verified against scratch Postgres 16 (create_all schema path):
- upgrade head applies cleanly on fresh + pre-seeded DBs
- DELETE FROM organization cascades to child rows (2 emails/1 phone/1 domain removed)
- downgrade -1 restores NO ACTION FKs
Author
Owner

Closing as superseded: the canonical fix for #96 has landed on main via watcher recovery commit 8a891e7 (migration b2c3d4e5f6a8_add_ondelete_cascade_org_child_fks.py, 173 lines). That migration mirrors the proven f101c4scd0a1_add_ondelete_cascade_case_membership_fk pattern from #101 with an _fk_has_cascade guard for safe re-run / fresh-schema no-op. PR #103 and PR #108 are now both superseded.

Thanks for the earlier migration drafts — the discussion on idempotency and the data-preservation pattern informed the landed version.

Closing as superseded: the canonical fix for #96 has landed on `main` via watcher recovery commit `8a891e7` (migration `b2c3d4e5f6a8_add_ondelete_cascade_org_child_fks.py`, 173 lines). That migration mirrors the proven `f101c4scd0a1_add_ondelete_cascade_case_membership_fk` pattern from #101 with an `_fk_has_cascade` guard for safe re-run / fresh-schema no-op. PR #103 and PR #108 are now both superseded. Thanks for the earlier migration drafts — the discussion on idempotency and the data-preservation pattern informed the landed version.
Mark closed this pull request 2026-09-10 04:03:49 +00:00
Some checks failed
CI / Backend Lint & Format (pull_request) Failing after 4s
CI / Golden Fixture Validation (pull_request) Has been skipped
CI / Backend Tests (pull_request) Has been skipped
CI / Backend Security Tests (pull_request) Has been skipped
CI / Frontend Lint (pull_request) Failing after 22s
CI / Frontend Tests (pull_request) Has been skipped
CI / Dependency Security Scan (pull_request) Has been skipped
Stress Tests / Stress & Pipeline Tests (pull_request) Failing after 6s
Stress Tests / Full Data Pipeline Validation (pull_request) Has been skipped
Performance Regression Tests / Performance Regression Detection (pull_request) Failing after 33s
CI / Frontend E2E Tests (pull_request) Has been skipped
Performance Regression Tests / Compare with Baseline (pull_request) Has been skipped
CI / All Checks Passed (pull_request) Failing after 49s

Pull request closed

Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Mark/osint-hub!103
No description provided.