From e8286442e934fd2a834ca1c4664828e972a36b54 Mon Sep 17 00:00:00 2001 From: Maximiliano Osorio Date: Sun, 9 Aug 2026 21:07:27 -0400 Subject: [PATCH] fix(schema): cascade modeling provenance and permission on delete The user role may delete a problem statement, a task or a thread only while the row still carries a CREATE provenance event from that user. The provenance and permission FKs were ON DELETE RESTRICT, so that permission could never be satisfied: - delete the provenance first, and the row loses its own delete permission, so the delete matches 0 rows and returns success - delete the row first, and Postgres refuses it Both orders were run against a live database; neither works. Making the six FKs ON DELETE CASCADE breaks the tie. The client deletes only the row, while the CREATE event is still there to authorise it, and the dependants go with it. Provenance and permission rows have no meaning once their subject is gone, so cascading is also what they mean. Metadata is unchanged. Fixes https://github.com/mintproject/monorepo/issues/99 --- .../down.sql | 51 +++++++++++++++ .../up.sql | 65 +++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 migrations/1771200017000_modeling_provenance_cascade_on_delete/down.sql create mode 100644 migrations/1771200017000_modeling_provenance_cascade_on_delete/up.sql diff --git a/migrations/1771200017000_modeling_provenance_cascade_on_delete/down.sql b/migrations/1771200017000_modeling_provenance_cascade_on_delete/down.sql new file mode 100644 index 0000000..7cb2498 --- /dev/null +++ b/migrations/1771200017000_modeling_provenance_cascade_on_delete/down.sql @@ -0,0 +1,51 @@ +-- Restore ON DELETE RESTRICT on the modeling provenance and permission tables. +-- This reinstates the deadlock described in up.sql: the `user` role can no +-- longer delete a problem statement, a task or a thread by any order of +-- operations. Clients written against the cascade will start reporting the +-- delete as refused, which is the honest failure. + +BEGIN; + +ALTER TABLE public.problem_statement_provenance + DROP CONSTRAINT problem_statement_provenance_problem_statement_id_fkey; +ALTER TABLE public.problem_statement_provenance + ADD CONSTRAINT problem_statement_provenance_problem_statement_id_fkey + FOREIGN KEY (problem_statement_id) REFERENCES public.problem_statement(id) + ON UPDATE RESTRICT ON DELETE RESTRICT; + +ALTER TABLE public.problem_statement_permission + DROP CONSTRAINT problem_statement_permission_problem_statement_id_fkey; +ALTER TABLE public.problem_statement_permission + ADD CONSTRAINT problem_statement_permission_problem_statement_id_fkey + FOREIGN KEY (problem_statement_id) REFERENCES public.problem_statement(id) + ON UPDATE RESTRICT ON DELETE RESTRICT; + +ALTER TABLE public.task_provenance + DROP CONSTRAINT task_provenance_task_id_fkey; +ALTER TABLE public.task_provenance + ADD CONSTRAINT task_provenance_task_id_fkey + FOREIGN KEY (task_id) REFERENCES public.task(id) + ON UPDATE RESTRICT ON DELETE RESTRICT; + +ALTER TABLE public.task_permission + DROP CONSTRAINT task_permission_task_id_fkey; +ALTER TABLE public.task_permission + ADD CONSTRAINT task_permission_task_id_fkey + FOREIGN KEY (task_id) REFERENCES public.task(id) + ON UPDATE RESTRICT ON DELETE RESTRICT; + +ALTER TABLE public.thread_provenance + DROP CONSTRAINT thread_provenance_thread_id_fkey; +ALTER TABLE public.thread_provenance + ADD CONSTRAINT thread_provenance_thread_id_fkey + FOREIGN KEY (thread_id) REFERENCES public.thread(id) + ON UPDATE RESTRICT ON DELETE RESTRICT; + +ALTER TABLE public.thread_permission + DROP CONSTRAINT thread_permission_thread_id_fkey; +ALTER TABLE public.thread_permission + ADD CONSTRAINT thread_permission_thread_id_fkey + FOREIGN KEY (thread_id) REFERENCES public.thread(id) + ON UPDATE RESTRICT ON DELETE RESTRICT; + +COMMIT; diff --git a/migrations/1771200017000_modeling_provenance_cascade_on_delete/up.sql b/migrations/1771200017000_modeling_provenance_cascade_on_delete/up.sql new file mode 100644 index 0000000..596dc2e --- /dev/null +++ b/migrations/1771200017000_modeling_provenance_cascade_on_delete/up.sql @@ -0,0 +1,65 @@ +-- Let a problem statement, task or thread carry its provenance and permission +-- rows to the grave, instead of requiring the client to remove them first. +-- +-- The `user` role may delete one of these rows only if the row still has a +-- CREATE provenance event from that user: +-- +-- filter: { events: { _and: [ {event: {_eq: CREATE}}, {userid: {_eq: X-Hasura-User-Id}} ] } } +-- +-- With ON DELETE RESTRICT that permission was unsatisfiable. Deleting the +-- provenance first (what every client does) removed the row's own delete +-- permission, so the delete matched 0 rows and returned success. Deleting the +-- row first was refused by this constraint. Neither order could work. +-- +-- CASCADE breaks the tie: the client deletes only the row, while its CREATE +-- event is still there to authorise the delete, and Postgres removes the +-- dependants. Provenance and permission rows have no life of their own once +-- their subject is gone, so cascading is also what they mean. +-- +-- See https://github.com/mintproject/monorepo/issues/99 + +BEGIN; + +ALTER TABLE public.problem_statement_provenance + DROP CONSTRAINT problem_statement_provenance_problem_statement_id_fkey; +ALTER TABLE public.problem_statement_provenance + ADD CONSTRAINT problem_statement_provenance_problem_statement_id_fkey + FOREIGN KEY (problem_statement_id) REFERENCES public.problem_statement(id) + ON UPDATE RESTRICT ON DELETE CASCADE; + +ALTER TABLE public.problem_statement_permission + DROP CONSTRAINT problem_statement_permission_problem_statement_id_fkey; +ALTER TABLE public.problem_statement_permission + ADD CONSTRAINT problem_statement_permission_problem_statement_id_fkey + FOREIGN KEY (problem_statement_id) REFERENCES public.problem_statement(id) + ON UPDATE RESTRICT ON DELETE CASCADE; + +ALTER TABLE public.task_provenance + DROP CONSTRAINT task_provenance_task_id_fkey; +ALTER TABLE public.task_provenance + ADD CONSTRAINT task_provenance_task_id_fkey + FOREIGN KEY (task_id) REFERENCES public.task(id) + ON UPDATE RESTRICT ON DELETE CASCADE; + +ALTER TABLE public.task_permission + DROP CONSTRAINT task_permission_task_id_fkey; +ALTER TABLE public.task_permission + ADD CONSTRAINT task_permission_task_id_fkey + FOREIGN KEY (task_id) REFERENCES public.task(id) + ON UPDATE RESTRICT ON DELETE CASCADE; + +ALTER TABLE public.thread_provenance + DROP CONSTRAINT thread_provenance_thread_id_fkey; +ALTER TABLE public.thread_provenance + ADD CONSTRAINT thread_provenance_thread_id_fkey + FOREIGN KEY (thread_id) REFERENCES public.thread(id) + ON UPDATE RESTRICT ON DELETE CASCADE; + +ALTER TABLE public.thread_permission + DROP CONSTRAINT thread_permission_thread_id_fkey; +ALTER TABLE public.thread_permission + ADD CONSTRAINT thread_permission_thread_id_fkey + FOREIGN KEY (thread_id) REFERENCES public.thread(id) + ON UPDATE RESTRICT ON DELETE CASCADE; + +COMMIT;