Files
PiCloud/crates/manager-core/migrations/0057_group_routes.sql
MechaCat02 0a583aa467 feat(routes): polymorphic owner on routes for group templates (§11 tail R1)
A group-owned route is a TEMPLATE, expanded into every descendant app's
in-memory RouteTable slice at rebuild (live, no materialization). The
schema reshape mirrors 0056_group_triggers exactly: a nullable group_id
FK→groups ON DELETE RESTRICT (a route template is a binding, not data),
app_id made nullable, an exactly-one owner CHECK, and the per-app unique
binding index split into per-owner partials. A group can't declare two
identical templates; an app declaring an identical binding is a
deliberate shadow resolved at rebuild (nearest-owner-wins), not a DB
conflict. Adds routes_group_id_idx for the expansion join.

Schema blessed at 57 migrations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 21:25:22 +02:00

49 lines
2.4 KiB
SQL

-- §11 tail (v1.2 Hierarchies): group ROUTE templates.
--
-- Until now every route was owned by exactly one app (`app_id NOT NULL`).
-- A group-owned route is a TEMPLATE: it is never served directly, but the
-- in-memory RouteTable rebuild EXPANDS it into every descendant app's slice
-- via the ancestor chain (live resolution, like vars/secrets/scripts, §11.6
-- collections, and §11 trigger templates — no per-app materialized rows). It
-- binds a group-owned handler script and runs under each firing app's
-- `app_id`.
--
-- Unlike triggers (a SQL-per-event dispatch), routes are served from an
-- in-memory cache, so the inheritance is resolved at table-rebuild time, not
-- per request. The reshape, however, is identical: a polymorphic owner column.
--
-- Reshape mirrors 0056_group_triggers exactly (RESTRICT, not CASCADE — a route
-- 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.
-- * the per-app unique binding index becomes per-owner partials.
ALTER TABLE routes
ADD COLUMN group_id UUID REFERENCES groups(id) ON DELETE RESTRICT;
ALTER TABLE routes
ALTER COLUMN app_id DROP NOT NULL;
ALTER TABLE routes
ADD CONSTRAINT routes_owner_exactly_one
CHECK ((group_id IS NULL) <> (app_id IS NULL));
-- Per-owner binding uniqueness (partial so each owner column only constrains
-- its own rows). Existing app rows keep the exact binding-tuple uniqueness; a
-- group can't declare two identical templates. An app declaring a binding
-- identical to an inherited group template is a deliberate SHADOW, resolved
-- at rebuild (nearest-owner-wins) — not a DB conflict.
DROP INDEX routes_unique_binding_idx;
CREATE UNIQUE INDEX routes_unique_binding_idx
ON routes (app_id, host_kind, host, path_kind, path, COALESCE(method, ''))
WHERE app_id IS NOT NULL;
CREATE UNIQUE INDEX routes_group_binding_idx
ON routes (group_id, host_kind, host, path_kind, path, COALESCE(method, ''))
WHERE group_id IS NOT NULL;
-- The expansion join matches routes by owner (`r.app_id = ac.owner_app OR
-- r.group_id = ac.owner_group`). The app side already has routes_app_id_idx;
-- add the parallel group lookup index.
CREATE INDEX routes_group_id_idx ON routes (group_id) WHERE group_id IS NOT NULL;