feat(modules): polymorphic owner on triggers for group templates (§11 tail T1)

Reshape the triggers table to allow a GROUP owner, mirroring
0050_group_scripts exactly. A group-owned trigger is a TEMPLATE: never
dispatched directly, but unioned into every descendant app's match
queries via the ancestor-chain CTE (live, no per-app materialization).

- 0056_group_triggers.sql: add nullable group_id (FK→groups ON DELETE
  RESTRICT), make app_id nullable, add the exactly-one owner CHECK, and
  split the name-unique + dispatch-hot indexes into per-owner partials
  (idx_triggers_group_kind_enabled serves the chain-union lookups).
- Detail tables unchanged (they hang off trigger_id).

Schema snapshot blessed (56 migrations); existing trigger tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-30 20:20:21 +02:00
parent e885ed5412
commit 7f51087d5d
2 changed files with 58 additions and 3 deletions

View File

@@ -385,7 +385,7 @@ table: topics
table: triggers
id: uuid NOT NULL default=gen_random_uuid()
app_id: uuid NOT NULL
app_id: uuid NULL
script_id: uuid NOT NULL
kind: text NOT NULL
enabled: boolean NOT NULL default=true
@@ -397,6 +397,7 @@ table: triggers
created_at: timestamp with time zone NOT NULL default=now()
updated_at: timestamp with time zone NOT NULL default=now()
name: text NOT NULL default=(gen_random_uuid())::text
group_id: uuid NULL
table: vars
id: uuid NOT NULL default=gen_random_uuid()
@@ -605,10 +606,12 @@ indexes on topics:
topics_pkey: public.topics USING btree (app_id, name)
indexes on triggers:
idx_triggers_app_kind_enabled: public.triggers USING btree (app_id, kind) WHERE (enabled = true)
idx_triggers_app_kind_enabled: public.triggers USING btree (app_id, kind) WHERE ((enabled = true) AND (app_id IS NOT NULL))
idx_triggers_app_pubsub_enabled: public.triggers USING btree (app_id, kind) WHERE ((enabled = true) AND (kind = 'pubsub'::text))
idx_triggers_group_kind_enabled: public.triggers USING btree (group_id, kind) WHERE ((enabled = true) AND (group_id IS NOT NULL))
idx_triggers_kind_enabled: public.triggers USING btree (kind) WHERE (enabled = true)
triggers_app_name_uniq: public.triggers USING btree (app_id, name)
triggers_app_name_uniq: public.triggers USING btree (app_id, name) WHERE (app_id IS NOT NULL)
triggers_group_name_uniq: public.triggers USING btree (group_id, name) WHERE (group_id IS NOT NULL)
triggers_pkey: public.triggers USING btree (id)
indexes on vars:
@@ -833,8 +836,10 @@ constraints on topics:
constraints on triggers:
[CHECK] triggers_dispatch_mode_check: CHECK ((dispatch_mode = ANY (ARRAY['sync'::text, 'async'::text])))
[CHECK] triggers_kind_check: CHECK ((kind = ANY (ARRAY['kv'::text, 'dead_letter'::text, 'docs'::text, 'cron'::text, 'files'::text, 'pubsub'::text, 'email'::text, 'queue'::text])))
[CHECK] triggers_owner_exactly_one: CHECK (((group_id IS NULL) <> (app_id IS NULL)))
[CHECK] triggers_retry_backoff_check: CHECK ((retry_backoff = ANY (ARRAY['exponential'::text, 'linear'::text, 'constant'::text])))
[FOREIGN KEY] triggers_app_id_fkey: FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE
[FOREIGN KEY] triggers_group_id_fkey: FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE RESTRICT
[FOREIGN KEY] triggers_registered_by_principal_fkey: FOREIGN KEY (registered_by_principal) REFERENCES admin_users(id) ON DELETE CASCADE
[FOREIGN KEY] triggers_script_id_fkey: FOREIGN KEY (script_id) REFERENCES scripts(id) ON DELETE CASCADE
[PRIMARY KEY] triggers_pkey: PRIMARY KEY (id)
@@ -901,3 +906,4 @@ constraints on vars:
0053: group kv entries
0054: group docs
0055: group files
0056: group triggers