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>
This commit is contained in:
MechaCat02
2026-06-30 21:25:22 +02:00
parent 95007c124e
commit 0a583aa467
2 changed files with 56 additions and 2 deletions

View File

@@ -338,9 +338,10 @@ table: routes
path: text NOT NULL
method: text NULL
created_at: timestamp with time zone NOT NULL default=now()
app_id: uuid NOT NULL
app_id: uuid NULL
dispatch_mode: text NOT NULL default='sync'::text
enabled: boolean NOT NULL default=true
group_id: uuid NULL
table: script_imports
app_id: uuid NOT NULL
@@ -578,10 +579,12 @@ indexes on queue_trigger_details:
indexes on routes:
routes_app_id_idx: public.routes USING btree (app_id)
routes_group_binding_idx: public.routes USING btree (group_id, host_kind, host, path_kind, path, COALESCE(method, ''::text)) WHERE (group_id IS NOT NULL)
routes_group_id_idx: public.routes USING btree (group_id) WHERE (group_id IS NOT NULL)
routes_lookup_idx: public.routes USING btree (host_kind, host)
routes_pkey: public.routes USING btree (id)
routes_script_id_idx: public.routes USING btree (script_id)
routes_unique_binding_idx: public.routes USING btree (app_id, host_kind, host, path_kind, path, COALESCE(method, ''::text))
routes_unique_binding_idx: public.routes USING btree (app_id, host_kind, host, path_kind, path, COALESCE(method, ''::text)) WHERE (app_id IS NOT NULL)
indexes on script_imports:
idx_script_imports_app: public.script_imports USING btree (app_id)
@@ -802,8 +805,10 @@ constraints on queue_trigger_details:
constraints on routes:
[CHECK] routes_dispatch_mode_check: CHECK ((dispatch_mode = ANY (ARRAY['sync'::text, 'async'::text])))
[CHECK] routes_host_kind_check: CHECK ((host_kind = ANY (ARRAY['any'::text, 'strict'::text, 'wildcard'::text])))
[CHECK] routes_owner_exactly_one: CHECK (((group_id IS NULL) <> (app_id IS NULL)))
[CHECK] routes_path_kind_check: CHECK ((path_kind = ANY (ARRAY['exact'::text, 'prefix'::text, 'param'::text])))
[FOREIGN KEY] routes_app_id_fk: FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE
[FOREIGN KEY] routes_group_id_fkey: FOREIGN KEY (group_id) REFERENCES groups(id) ON DELETE RESTRICT
[FOREIGN KEY] routes_script_id_fkey: FOREIGN KEY (script_id) REFERENCES scripts(id) ON DELETE CASCADE
[PRIMARY KEY] routes_pkey: PRIMARY KEY (id)
@@ -907,3 +912,4 @@ constraints on vars:
0054: group docs
0055: group files
0056: group triggers
0057: group routes