From cdef97a634b0dc94e31a0686738b0633d016edb4 Mon Sep 17 00:00:00 2001 From: MechaCat02 Date: Wed, 1 Jul 2026 20:00:44 +0200 Subject: [PATCH] feat(suppress): polymorphic owner on template_suppressions (M1.1) Reshape the app-only suppression marker to a group/app polymorphic owner (mirrors 0056/0057 + the 0051/0052 config markers): nullable group_id (CASCADE), nullable app_id, exactly-one CHECK, per-owner partial unique indexes. Lets a [group] decline a template it inherits from a higher ancestor for its whole subtree; consumption filters land in M1.3/M1.4. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../migrations/0060_group_suppressions.sql | 38 +++++++++++++++++++ crates/manager-core/tests/expected_schema.txt | 10 ++++- 2 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 crates/manager-core/migrations/0060_group_suppressions.sql diff --git a/crates/manager-core/migrations/0060_group_suppressions.sql b/crates/manager-core/migrations/0060_group_suppressions.sql new file mode 100644 index 0000000..b749621 --- /dev/null +++ b/crates/manager-core/migrations/0060_group_suppressions.sql @@ -0,0 +1,38 @@ +-- §11 tail (v1.2 Hierarchies): GROUP-level template suppression. +-- +-- 0058 let an APP decline an inherited group template (for that app only). A +-- group operator often wants the same opt-out for a whole subtree: "no +-- descendant of this group runs the ancestor's `audit` template." Until now +-- only an app could suppress; a group could not. +-- +-- Reshape `template_suppressions` to a POLYMORPHIC owner, exactly like the +-- group triggers/routes reshape (0056/0057) and the config markers (0051/0052): +-- * `group_id` — nullable FK→groups, ON DELETE CASCADE (config, not code). +-- * `app_id` — made nullable; the exactly-one CHECK enforces app XOR group. +-- * the per-app unique index becomes per-owner partials. +-- +-- A GROUP suppression declines a template the group INHERITS from a higher +-- ancestor, for the group's whole subtree. Inheritance-only still holds: the +-- dispatch filters gate to group-owned rows, and a suppression only matches on +-- the suppressing owner's ancestor chain — a group can decline what it +-- inherits, never a sibling subtree's, never its own descendants' own rows. + +ALTER TABLE template_suppressions + ADD COLUMN group_id UUID REFERENCES groups(id) ON DELETE CASCADE; + +ALTER TABLE template_suppressions + ALTER COLUMN app_id DROP NOT NULL; + +ALTER TABLE template_suppressions + ADD CONSTRAINT template_suppressions_owner_exactly_one + CHECK ((group_id IS NULL) <> (app_id IS NULL)); + +DROP INDEX template_suppressions_uidx; +CREATE UNIQUE INDEX template_suppressions_app_uidx + ON template_suppressions (app_id, target_kind, reference) WHERE app_id IS NOT NULL; +CREATE UNIQUE INDEX template_suppressions_group_uidx + ON template_suppressions (group_id, target_kind, reference) WHERE group_id IS NOT NULL; + +-- The trigger anti-join / route rebuild look up by both owners now. +CREATE INDEX template_suppressions_group_idx + ON template_suppressions (group_id) WHERE group_id IS NOT NULL; diff --git a/crates/manager-core/tests/expected_schema.txt b/crates/manager-core/tests/expected_schema.txt index 64a16da..ef6a6af 100644 --- a/crates/manager-core/tests/expected_schema.txt +++ b/crates/manager-core/tests/expected_schema.txt @@ -379,10 +379,11 @@ table: secrets table: template_suppressions id: uuid NOT NULL default=gen_random_uuid() - app_id: uuid NOT NULL + app_id: uuid NULL target_kind: text NOT NULL reference: text NOT NULL created_at: timestamp with time zone NOT NULL default=now() + group_id: uuid NULL table: topics app_id: uuid NOT NULL @@ -616,8 +617,10 @@ indexes on secrets: indexes on template_suppressions: template_suppressions_app_idx: public.template_suppressions USING btree (app_id) + template_suppressions_app_uidx: public.template_suppressions USING btree (app_id, target_kind, reference) WHERE (app_id IS NOT NULL) + template_suppressions_group_idx: public.template_suppressions USING btree (group_id) WHERE (group_id IS NOT NULL) + template_suppressions_group_uidx: public.template_suppressions USING btree (group_id, target_kind, reference) WHERE (group_id IS NOT NULL) template_suppressions_pkey: public.template_suppressions USING btree (id) - template_suppressions_uidx: public.template_suppressions USING btree (app_id, target_kind, reference) indexes on topics: topics_pkey: public.topics USING btree (app_id, name) @@ -848,8 +851,10 @@ constraints on secrets: [FOREIGN KEY] secrets_group_id_fkey: FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE constraints on template_suppressions: + [CHECK] template_suppressions_owner_exactly_one: CHECK (((group_id IS NULL) <> (app_id IS NULL))) [CHECK] template_suppressions_target_kind_check: CHECK ((target_kind = ANY (ARRAY['trigger'::text, 'route'::text]))) [FOREIGN KEY] template_suppressions_app_id_fkey: FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE + [FOREIGN KEY] template_suppressions_group_id_fkey: FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE CASCADE [PRIMARY KEY] template_suppressions_pkey: PRIMARY KEY (id) constraints on topics: @@ -934,3 +939,4 @@ constraints on vars: 0057: group routes 0058: template suppressions 0059: sealed templates + 0060: group suppressions