Files
PiCloud/crates/manager-core/migrations/0048_vars.sql
MechaCat02 35dbd9f368 feat(config): vars schema + the §3 resolution engine
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>
2026-06-24 20:43:16 +02:00

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;