Files
PiCloud/crates/manager-core/migrations/0056_group_triggers.sql
MechaCat02 7f51087d5d 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>
2026-06-30 20:20:21 +02:00

50 lines
2.5 KiB
SQL

-- §11 tail (v1.2 Hierarchies): group TRIGGER templates.
--
-- Until now every trigger was owned by exactly one app (`app_id NOT NULL`).
-- A group-owned trigger is a TEMPLATE: it is never dispatched directly, but
-- the dispatcher's hot-path match queries UNION it in for every descendant app
-- via the ancestor-chain CTE (live resolution, like vars/secrets/scripts and
-- §11.6 collections — no per-app materialized rows). It binds a group-owned
-- handler script and fires under each firing app's `app_id`.
--
-- Scope: EVENT kinds only (kv/docs/files/pubsub) — those are stateless at
-- dispatch. cron/queue/email carry per-instance state (last_fired_at, the
-- one-consumer advisory lock, a sealed inbound secret) and would need
-- materialization; they are rejected on a [group] at the manifest layer and
-- deferred. The CHECK here is deliberately NOT narrowed to event kinds: the
-- column allows any kind for forward-compat, the authoring gate is upstream.
--
-- Reshape mirrors 0050_group_scripts exactly (RESTRICT, not CASCADE — a
-- trigger template references a group script; a group can't be deleted out
-- from under it):
-- * `group_id` — nullable FK→groups, ON DELETE RESTRICT.
-- * `app_id` — made nullable; the exactly-one CHECK enforces app XOR group.
-- * per-app unique name index + dispatch index become per-owner partials.
ALTER TABLE triggers
ADD COLUMN group_id UUID REFERENCES groups(id) ON DELETE RESTRICT;
ALTER TABLE triggers
ALTER COLUMN app_id DROP NOT NULL;
ALTER TABLE triggers
ADD CONSTRAINT triggers_owner_exactly_one
CHECK ((group_id IS NULL) <> (app_id IS NULL));
-- Per-owner name uniqueness (partial so each owner column only constrains its
-- own rows). Existing app rows keep the exact (app_id, name) uniqueness.
DROP INDEX triggers_app_name_uniq;
CREATE UNIQUE INDEX triggers_app_name_uniq
ON triggers (app_id, name) WHERE app_id IS NOT NULL;
CREATE UNIQUE INDEX triggers_group_name_uniq
ON triggers (group_id, name) WHERE group_id IS NOT NULL;
-- Per-owner dispatch hot-path index ("all enabled triggers of kind Y for
-- owner X"). The app index keeps its name + shape (now owner-partial); a
-- parallel group index serves the chain-union's group-template lookups.
DROP INDEX idx_triggers_app_kind_enabled;
CREATE INDEX idx_triggers_app_kind_enabled
ON triggers (app_id, kind) WHERE enabled = TRUE AND app_id IS NOT NULL;
CREATE INDEX idx_triggers_group_kind_enabled
ON triggers (group_id, kind) WHERE enabled = TRUE AND group_id IS NOT NULL;