Phase 3 foundation (docs §3): env-filtered, proximity-first config inheritance down the group tree. - Migration 0048: `vars` (polymorphic group|app owner via two nullable FKs + CHECK exactly-one, env scope, JSONB value, explicit tombstone) + `apps.environment` (the env marker the resolver filters on — 'an environment is an app'). - config_resolver: a shared chain-walk CTE (mirrors effective_app_role) that walks app → ancestor groups, env-filters in SQL, plus a pure Rust resolution pass implementing the §3 semantics SQL can't — per-key proximity-first, @E-over-* within a level, map deep-merge, tombstone suppression, and provenance for --explain. Verified: 8 unit tests (incl. the §3.2 proximity-beats-env-specificity call, deep-merge, tombstone, boundary) + the candidate-fetch CTE against live Postgres (env-filter drops a non-matching @production value; nearer level shadows farther). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
48 lines
2.4 KiB
SQL
48 lines
2.4 KiB
SQL
-- Phase 3: group-inherited config — the `vars` table + an app environment
|
|
-- marker. See docs/design/groups-and-project-tool.md §3, §5.1.
|
|
--
|
|
-- `vars` is the net-new, env-scoped configuration layer (greenfield — there
|
|
-- is no env-agnostic config today, only `secrets`). A var is owned by
|
|
-- exactly one group OR one app, optionally scoped to an environment, and
|
|
-- resolved down the tree (§3): env-filter first, then nearest level wins.
|
|
--
|
|
-- "An environment is an app": each env is already a distinct app row. To
|
|
-- env-filter group-level `@E` values, the resolver must know which env an
|
|
-- app represents — recorded here on `apps.environment` (NULL = env-agnostic,
|
|
-- set at app-create from the CLI's known env).
|
|
|
|
ALTER TABLE apps ADD COLUMN environment TEXT;
|
|
|
|
CREATE TABLE vars (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
-- Polymorphic owner: exactly one of the two FKs is set. Real FKs (not a
|
|
-- bare owner_id) so ON DELETE CASCADE works per owner kind and a dangling
|
|
-- owner is impossible.
|
|
group_id UUID REFERENCES groups(id) ON DELETE CASCADE,
|
|
app_id UUID REFERENCES apps(id) ON DELETE CASCADE,
|
|
CONSTRAINT vars_owner_exactly_one
|
|
CHECK ((group_id IS NULL) <> (app_id IS NULL)),
|
|
-- '*' = env-agnostic; otherwise an env name matched against
|
|
-- apps.environment. NOT NULL with a '*' sentinel so the uniqueness
|
|
-- indexes are total (a NULL scope would break UNIQUE).
|
|
environment_scope TEXT NOT NULL DEFAULT '*',
|
|
key TEXT NOT NULL,
|
|
-- JSONB per the v1.1 data-plane convention. A tombstone (deletion of an
|
|
-- inherited key, §3) is the explicit boolean below, NOT value = 'null'
|
|
-- — JSON null is a legitimate value and must stay distinguishable.
|
|
value JSONB NOT NULL,
|
|
is_tombstone BOOLEAN NOT NULL DEFAULT FALSE,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
-- One row per (owner, scope, key). Partial unique indexes because the owner
|
|
-- is split across two nullable columns.
|
|
CREATE UNIQUE INDEX vars_group_uidx
|
|
ON vars (group_id, environment_scope, key) WHERE group_id IS NOT NULL;
|
|
CREATE UNIQUE INDEX vars_app_uidx
|
|
ON vars (app_id, environment_scope, key) WHERE app_id IS NOT NULL;
|
|
|
|
CREATE INDEX vars_group_id_idx ON vars (group_id) WHERE group_id IS NOT NULL;
|
|
CREATE INDEX vars_app_id_idx ON vars (app_id) WHERE app_id IS NOT NULL;
|