feat(analytics): sql/009 — execution metadata, metric governance, semantic registry, quarantine, usage ledger
CI (SIAX Cloud) / integration (push) Failing after 38s
CI (SIAX Cloud) / quality (push) Successful in 1m0s
CI (SIAX Cloud) / security (push) Successful in 25s
Security Scan (SIAX Cloud) / security (push) Successful in 28s
CI (SIAX Cloud) / integration (pull_request) Failing after 48s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m16s
CI (SIAX Cloud) / security (pull_request) Successful in 27s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 23s
CI (SIAX Cloud) / integration (push) Failing after 38s
CI (SIAX Cloud) / quality (push) Successful in 1m0s
CI (SIAX Cloud) / security (push) Successful in 25s
Security Scan (SIAX Cloud) / security (push) Successful in 28s
CI (SIAX Cloud) / integration (pull_request) Failing after 48s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m16s
CI (SIAX Cloud) / security (pull_request) Successful in 27s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 23s
The migration the previous commits' comments promised. Additive and idempotent
throughout, so the restore drill can replay it onto a live database.
BI-P018 adds principal_id, workload, compiled_query_hash, datasets, metrics,
cache_status and cost_units to the execution record — and deliberately NO
`query_text` column, with a CHECK pinning the hash to 64 hex characters so a
statement smuggled into that field is refused by the database. A test asserts
the absence of `query_text` and `sql` as columns.
BI-P031's DoD gets a Postgres TRIGGER, not just the TypeScript guard: a
certified metric whose meaning_hash changes without a version bump raises. That
is the layer that holds when something writes without going through the
application — a migration script, a psql session, an admin tool. A compiler
opinion does not survive `psql`; a trigger does.
BI-P030 gets a version-history table whose CONTENT is immutable (status and
valid_from may still move) plus a PARTIAL UNIQUE INDEX enforcing at most one
ACTIVE version per model per tenant. Two active versions is a silent
correctness failure in which half the dashboards read a different model, so it
is an index rather than application discipline.
BI-P021's usage ledger is append-only by trigger: update and delete both raise.
BI-P026's freshness table DEFAULTS TO 'UNKNOWN' — a dataset nobody has computed
freshness for must not read as FRESH — and requires a non-empty disclosure,
because a state with no words attached is a state a dashboard renders as a
silent green dot. BI-P025's quarantine record requires a principal and a reason
on release.
A BUG IN THIS MIGRATION, FOUND BY RUNNING IT:
`SELECT 1 FROM pg_constraint WHERE conname = '...'` is NOT schema-scoped.
conname is unique per schema, not per database, so on a database that already
had these tables in `public` every guarded CHECK was silently skipped when
the file was applied into a second schema. The constraints simply were not
there, and the tests that should have proven them PASSED — because the
INSERTs they expected to fail succeeded, and `expect(message).toContain(...)`
on an undefined message reads as a type error, not as "the constraint is
missing". Every guard now scopes via `conrelid = '<table>'::regclass`, and
the fix is proven by applying the file to `public` FIRST and then running the
suite into a fresh schema: 20/20.
Two flaws in my own test harness, also found by running it:
* `sql.unsafe("SET search_path ...; SELECT ...")` is a multi-statement simple
query, and postgres.js returns the LAST statement's result — so a SELECT
silently returned the connection object and two assertions compared against
nonsense. search_path is now pinned on the CONNECTION.
* the fixture INSERT used column names that do not exist in sql/008's dataset
table, which surfaced as a suite-level failure rather than as 20 skips.
20 tests against a real Postgres 16, gated on ANALYTICS_TEST_DATABASE_URL, and
each of them attempts the write the constraint exists to refuse.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,346 @@
|
||||
-- 0pt analytics: execution metadata, metric governance, semantic registry,
|
||||
-- quarantine (BI-P018, BI-P021, BI-P025, BI-P028, BI-P030, BI-P031).
|
||||
--
|
||||
-- Extends sql/008. Every statement is ADDITIVE and idempotent: ALTER TABLE ...
|
||||
-- ADD COLUMN IF NOT EXISTS, CREATE TABLE IF NOT EXISTS, CREATE OR REPLACE
|
||||
-- FUNCTION, DROP TRIGGER IF EXISTS before CREATE TRIGGER. It can be replayed
|
||||
-- onto an already-migrated database, which is what the restore drill needs.
|
||||
--
|
||||
-- Ordering: requires sql/008 (it alters two of its tables).
|
||||
--
|
||||
-- WHY COLUMNS RATHER THAN NEW TABLES for the metric fields: BI-P028 names
|
||||
-- fifteen fields and BI-P002 already persists nine of them. A second metric
|
||||
-- table would be exactly the failure BI-P028's DoD is about ("no production
|
||||
-- metric exists only inside a dashboard tool" generalises to "one metric, one
|
||||
-- home"), so the six missing fields are added where the metric already lives.
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- BI-P018 — QUERY EXECUTION MODEL
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- The fields BI-P018 lists that sql/008's QueryExecution did not carry.
|
||||
--
|
||||
-- `compiled_query_hash` and NOT the statement: BI-P018 says "without
|
||||
-- unnecessarily storing sensitive raw SQL". There is deliberately no
|
||||
-- `query_text` column, and the CHECK below makes adding one by accident
|
||||
-- visible — a hash is 64 hex characters and nothing else.
|
||||
ALTER TABLE opt_analytics_query_execution
|
||||
ADD COLUMN IF NOT EXISTS principal_id TEXT,
|
||||
ADD COLUMN IF NOT EXISTS workload TEXT,
|
||||
ADD COLUMN IF NOT EXISTS compiled_query_hash TEXT,
|
||||
ADD COLUMN IF NOT EXISTS datasets TEXT[] NOT NULL DEFAULT '{}',
|
||||
ADD COLUMN IF NOT EXISTS metrics TEXT[] NOT NULL DEFAULT '{}',
|
||||
ADD COLUMN IF NOT EXISTS cache_status TEXT NOT NULL DEFAULT 'miss',
|
||||
ADD COLUMN IF NOT EXISTS cost_units NUMERIC(20, 6);
|
||||
|
||||
-- NOTE ON THE GUARD SHAPE. `pg_constraint.conname` is NOT unique across
|
||||
-- schemas: a bare `WHERE conname = '...'` matches a same-named constraint in
|
||||
-- ANY schema and silently skips the ADD here. Found by running this file into
|
||||
-- a second schema on a database that already had it in `public` — every CHECK
|
||||
-- below was quietly absent, and the tests that should have proven them passed
|
||||
-- because the INSERTs they expected to fail succeeded. Every guard therefore
|
||||
-- scopes to the table via `conrelid = '<table>'::regclass`, which resolves
|
||||
-- through search_path.
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'opt_analytics_query_execution_hash_shape'
|
||||
AND conrelid = 'opt_analytics_query_execution'::regclass
|
||||
) THEN
|
||||
ALTER TABLE opt_analytics_query_execution
|
||||
ADD CONSTRAINT opt_analytics_query_execution_hash_shape
|
||||
CHECK (compiled_query_hash IS NULL OR compiled_query_hash ~ '^[0-9a-f]{64}$');
|
||||
END IF;
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'opt_analytics_query_execution_cache_status'
|
||||
AND conrelid = 'opt_analytics_query_execution'::regclass
|
||||
) THEN
|
||||
ALTER TABLE opt_analytics_query_execution
|
||||
ADD CONSTRAINT opt_analytics_query_execution_cache_status
|
||||
CHECK (cache_status IN ('hit','miss','bypass','stale_refused','not_cacheable'));
|
||||
END IF;
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint
|
||||
WHERE conname = 'opt_analytics_query_execution_workload'
|
||||
AND conrelid = 'opt_analytics_query_execution'::regclass
|
||||
) THEN
|
||||
ALTER TABLE opt_analytics_query_execution
|
||||
ADD CONSTRAINT opt_analytics_query_execution_workload
|
||||
CHECK (workload IS NULL OR workload IN
|
||||
('dashboard','report','api','analyst_sql','ai_analyst','scheduled','internal'));
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- Cost reporting is per tenant per window; index for it rather than making
|
||||
-- every billing query a sequential scan of every tenant's executions.
|
||||
CREATE INDEX IF NOT EXISTS opt_analytics_query_execution_cost_idx
|
||||
ON opt_analytics_query_execution (tenant_id, submitted_at DESC)
|
||||
WHERE cost_units IS NOT NULL;
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- BI-P017 — ANALYST SQL STATEMENT HISTORY
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- The ONE place raw SQL is retained, and it is a separate table with a
|
||||
-- separate retention posture precisely so that decision is visible.
|
||||
--
|
||||
-- `statement_normalized` (literals masked) is NOT NULL: it is always present
|
||||
-- and is the searchable, non-PII form. `statement_raw` is nullable, so a
|
||||
-- deployment that chooses not to retain it simply leaves it NULL rather than
|
||||
-- losing the history entirely.
|
||||
CREATE TABLE IF NOT EXISTS opt_analytics_sql_statement (
|
||||
statement_id TEXT PRIMARY KEY,
|
||||
tenant_id TEXT NOT NULL,
|
||||
project_id TEXT,
|
||||
principal_id TEXT NOT NULL,
|
||||
statement_normalized TEXT NOT NULL,
|
||||
statement_raw TEXT,
|
||||
statement_hash TEXT NOT NULL CHECK (statement_hash ~ '^[0-9a-f]{64}$'),
|
||||
referenced_objects TEXT[] NOT NULL DEFAULT '{}',
|
||||
outcome TEXT NOT NULL CHECK (outcome IN ('admitted','refused','succeeded','failed','cancelled')),
|
||||
error_code TEXT,
|
||||
refusal_reason TEXT,
|
||||
submitted_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
finished_at TIMESTAMPTZ,
|
||||
duration_ms INTEGER,
|
||||
rows_returned BIGINT,
|
||||
rows_read BIGINT,
|
||||
bytes_read BIGINT,
|
||||
correlation_id TEXT,
|
||||
-- A refusal must say why. An unreviewable refusal log is not an audit trail,
|
||||
-- and an analyst probing the boundary is exactly what an operator needs to see.
|
||||
CHECK (outcome <> 'refused' OR (refusal_reason IS NOT NULL AND length(btrim(refusal_reason)) > 0))
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS opt_analytics_sql_statement_tenant_idx
|
||||
ON opt_analytics_sql_statement (tenant_id, submitted_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS opt_analytics_sql_statement_refused_idx
|
||||
ON opt_analytics_sql_statement (tenant_id, submitted_at DESC)
|
||||
WHERE outcome = 'refused';
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- BI-P028 / BI-P031 — METRIC DOMAIN MODEL AND GOVERNANCE
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
ALTER TABLE opt_analytics_metric_definition
|
||||
ADD COLUMN IF NOT EXISTS metric_type TEXT NOT NULL DEFAULT 'simple',
|
||||
ADD COLUMN IF NOT EXISTS time_dimension TEXT,
|
||||
ADD COLUMN IF NOT EXISTS unit TEXT,
|
||||
ADD COLUMN IF NOT EXISTS currency TEXT,
|
||||
ADD COLUMN IF NOT EXISTS display_format TEXT NOT NULL DEFAULT 'decimal',
|
||||
ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'DRAFT',
|
||||
ADD COLUMN IF NOT EXISTS filters_json TEXT NOT NULL DEFAULT '[]',
|
||||
ADD COLUMN IF NOT EXISTS certification TEXT NOT NULL DEFAULT 'experimental',
|
||||
ADD COLUMN IF NOT EXISTS business_definition TEXT,
|
||||
ADD COLUMN IF NOT EXISTS technical_definition TEXT,
|
||||
ADD COLUMN IF NOT EXISTS meaning_hash TEXT,
|
||||
ADD COLUMN IF NOT EXISTS certified_at TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS certified_by TEXT;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'opt_analytics_metric_status'
|
||||
AND conrelid = 'opt_analytics_metric_definition'::regclass) THEN
|
||||
ALTER TABLE opt_analytics_metric_definition ADD CONSTRAINT opt_analytics_metric_status
|
||||
CHECK (status IN ('DRAFT','REVIEW','ACTIVE','DEPRECATED','RETIRED'));
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'opt_analytics_metric_type'
|
||||
AND conrelid = 'opt_analytics_metric_definition'::regclass) THEN
|
||||
ALTER TABLE opt_analytics_metric_definition ADD CONSTRAINT opt_analytics_metric_type
|
||||
CHECK (metric_type IN ('simple','ratio','derived','cumulative','conversion'));
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'opt_analytics_metric_format'
|
||||
AND conrelid = 'opt_analytics_metric_definition'::regclass) THEN
|
||||
ALTER TABLE opt_analytics_metric_definition ADD CONSTRAINT opt_analytics_metric_format
|
||||
CHECK (display_format IN ('integer','decimal','percent','currency','duration_ms','bytes'));
|
||||
END IF;
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'opt_analytics_metric_certification'
|
||||
AND conrelid = 'opt_analytics_metric_definition'::regclass) THEN
|
||||
ALTER TABLE opt_analytics_metric_definition ADD CONSTRAINT opt_analytics_metric_certification
|
||||
CHECK (certification IN ('experimental','certified','deprecated'));
|
||||
END IF;
|
||||
-- A currency-formatted metric with no currency is how SEK gets rendered as
|
||||
-- USD on a dashboard nobody double-checks.
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'opt_analytics_metric_currency_pairing'
|
||||
AND conrelid = 'opt_analytics_metric_definition'::regclass) THEN
|
||||
ALTER TABLE opt_analytics_metric_definition ADD CONSTRAINT opt_analytics_metric_currency_pairing
|
||||
CHECK (
|
||||
(display_format = 'currency' AND currency IS NOT NULL AND currency ~ '^[A-Z]{3}$')
|
||||
OR (display_format <> 'currency' AND currency IS NULL)
|
||||
);
|
||||
END IF;
|
||||
-- Certification is an ACT: it has an actor and a time, or it has not happened.
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'opt_analytics_metric_certification_act'
|
||||
AND conrelid = 'opt_analytics_metric_definition'::regclass) THEN
|
||||
ALTER TABLE opt_analytics_metric_definition ADD CONSTRAINT opt_analytics_metric_certification_act
|
||||
CHECK (
|
||||
certification <> 'certified'
|
||||
OR (certified_by IS NOT NULL AND certified_at IS NOT NULL
|
||||
AND business_definition IS NOT NULL AND technical_definition IS NOT NULL)
|
||||
);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ── BI-P031's DoD, in the DATABASE ────────────────────────────────────────
|
||||
-- "Certified metric cannot be modified in-place; new version required."
|
||||
--
|
||||
-- The TypeScript check (assertMetricEditAllowed) is layer 1. This trigger is
|
||||
-- layer 2, and it is the one that holds when something writes to the table
|
||||
-- without going through the application — a migration script, a psql session,
|
||||
-- an admin tool. A compiler opinion does not survive `psql`; a trigger does.
|
||||
--
|
||||
-- The rule is on the MEANING HASH, so a change this trigger has never heard of
|
||||
-- still counts as a change: new fields default to "changes the meaning" rather
|
||||
-- than to "allowed", which is the fail-closed direction.
|
||||
CREATE OR REPLACE FUNCTION opt_analytics_metric_certified_immutable() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
IF OLD.certification = 'certified'
|
||||
AND NEW.current_version = OLD.current_version
|
||||
AND NEW.meaning_hash IS DISTINCT FROM OLD.meaning_hash THEN
|
||||
RAISE EXCEPTION
|
||||
'BI-P031: metric % is CERTIFIED; its definition cannot be modified in place - mint version %',
|
||||
OLD.name, OLD.current_version + 1
|
||||
USING ERRCODE = 'restrict_violation';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS opt_analytics_metric_certified_guard ON opt_analytics_metric_definition;
|
||||
CREATE TRIGGER opt_analytics_metric_certified_guard
|
||||
BEFORE UPDATE ON opt_analytics_metric_definition
|
||||
FOR EACH ROW EXECUTE FUNCTION opt_analytics_metric_certified_immutable();
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- BI-P030 — SEMANTIC MODEL REGISTRY
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- Persisted SEPARATELY from any implementation engine, which is the whole
|
||||
-- point of the prompt. There is no column here for a provider manifest, and
|
||||
-- `assertOnlySemanticModelFields` refuses one in the application; the schema
|
||||
-- refuses it by simply not having anywhere to put it.
|
||||
--
|
||||
-- sql/008's opt_analytics_semantic_model holds the current model; this table
|
||||
-- holds the VERSION HISTORY BI-P030 enumerates, and its rows are immutable for
|
||||
-- the same reason a MetricVersion's are: a dashboard pinned to version 7 must
|
||||
-- mean the same thing tomorrow.
|
||||
CREATE TABLE IF NOT EXISTS opt_analytics_semantic_model_version (
|
||||
semantic_model_version_id TEXT PRIMARY KEY,
|
||||
model_id TEXT NOT NULL,
|
||||
tenant_id TEXT NOT NULL,
|
||||
project_id TEXT NOT NULL,
|
||||
version INTEGER NOT NULL CHECK (version >= 1),
|
||||
datasets TEXT[] NOT NULL DEFAULT '{}',
|
||||
relationships_json TEXT NOT NULL DEFAULT '[]',
|
||||
metrics_json TEXT NOT NULL DEFAULT '[]',
|
||||
dimensions_json TEXT NOT NULL DEFAULT '[]',
|
||||
valid_from TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
status TEXT NOT NULL CHECK (status IN ('draft','active','superseded','retired')),
|
||||
hash TEXT NOT NULL CHECK (hash ~ '^[0-9a-f]{64}$'),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
created_by TEXT,
|
||||
UNIQUE (tenant_id, model_id, version)
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS opt_analytics_semantic_model_version_tenant_idx
|
||||
ON opt_analytics_semantic_model_version (tenant_id, project_id, model_id, version DESC);
|
||||
|
||||
-- At most ONE active version per model per tenant. A partial unique index
|
||||
-- rather than application discipline: two active versions is a silent
|
||||
-- correctness failure in which half the dashboards read a different model.
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS opt_analytics_semantic_model_one_active
|
||||
ON opt_analytics_semantic_model_version (tenant_id, model_id)
|
||||
WHERE status = 'active';
|
||||
|
||||
-- A published version's CONTENT is immutable; only its status and valid_from
|
||||
-- may move (draft -> active -> superseded -> retired).
|
||||
CREATE OR REPLACE FUNCTION opt_analytics_semantic_version_content_immutable() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
IF NEW.hash IS DISTINCT FROM OLD.hash
|
||||
OR NEW.metrics_json IS DISTINCT FROM OLD.metrics_json
|
||||
OR NEW.dimensions_json IS DISTINCT FROM OLD.dimensions_json
|
||||
OR NEW.relationships_json IS DISTINCT FROM OLD.relationships_json
|
||||
OR NEW.datasets IS DISTINCT FROM OLD.datasets
|
||||
OR NEW.version IS DISTINCT FROM OLD.version THEN
|
||||
RAISE EXCEPTION
|
||||
'BI-P030: semantic model % v% is immutable; publish a new version instead',
|
||||
OLD.model_id, OLD.version
|
||||
USING ERRCODE = 'restrict_violation';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS opt_analytics_semantic_version_guard ON opt_analytics_semantic_model_version;
|
||||
CREATE TRIGGER opt_analytics_semantic_version_guard
|
||||
BEFORE UPDATE ON opt_analytics_semantic_model_version
|
||||
FOR EACH ROW EXECUTE FUNCTION opt_analytics_semantic_version_content_immutable();
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- BI-P025 / BI-P026 — QUARANTINE AND FRESHNESS
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- The quarantine record BI-P025's DoD acts on. The READ-side gate lives in the
|
||||
-- application (`assertProjectionReadable`); this is its durable state, so a
|
||||
-- process restart does not release a quarantine.
|
||||
CREATE TABLE IF NOT EXISTS opt_analytics_projection_quarantine (
|
||||
tenant_id TEXT NOT NULL,
|
||||
dataset_id TEXT NOT NULL REFERENCES opt_analytics_dataset (dataset_id),
|
||||
quarantined BOOLEAN NOT NULL,
|
||||
caused_by TEXT[] NOT NULL DEFAULT '{}',
|
||||
reason TEXT,
|
||||
since TIMESTAMPTZ,
|
||||
released_at TIMESTAMPTZ,
|
||||
released_by TEXT,
|
||||
PRIMARY KEY (tenant_id, dataset_id),
|
||||
-- Release is an ACT with an operator behind it, never an automatic
|
||||
-- consequence of a later passing run: a flapping rule would otherwise
|
||||
-- quarantine and un-quarantine a dataset between two reads of one dashboard.
|
||||
CHECK (quarantined OR released_at IS NULL OR (released_by IS NOT NULL AND reason IS NOT NULL))
|
||||
);
|
||||
|
||||
-- BI-P026 freshness state, per dataset. `UNKNOWN` is the DEFAULT, deliberately:
|
||||
-- a dataset nobody has computed freshness for must not read as FRESH.
|
||||
CREATE TABLE IF NOT EXISTS opt_analytics_dataset_freshness (
|
||||
tenant_id TEXT NOT NULL,
|
||||
dataset_id TEXT NOT NULL REFERENCES opt_analytics_dataset (dataset_id),
|
||||
state TEXT NOT NULL DEFAULT 'UNKNOWN'
|
||||
CHECK (state IN ('FRESH','DELAYED','STALE','FAILED','UNKNOWN')),
|
||||
age_ms BIGINT,
|
||||
last_successful_load_at TIMESTAMPTZ,
|
||||
last_run_at TIMESTAMPTZ,
|
||||
last_run_status TEXT CHECK (last_run_status IN ('succeeded','failed','running','never_run')),
|
||||
expected_interval_ms BIGINT NOT NULL,
|
||||
disclosure TEXT NOT NULL,
|
||||
computed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (tenant_id, dataset_id),
|
||||
-- Every state must carry a disclosure a UI can render verbatim. A state with
|
||||
-- no words attached is a state a dashboard will render as a silent green dot.
|
||||
CHECK (length(btrim(disclosure)) > 0)
|
||||
);
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- BI-P021 — USAGE LEDGER
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- What CL0UD quotas, AUD0 usage and B00K accounting read. Append-only.
|
||||
--
|
||||
-- `terminated_by_budget` is recorded because a query the budget killed still
|
||||
-- COST what it burned before it died; not charging for it is how a tenant
|
||||
-- learns that the cheapest query is one that gets cancelled.
|
||||
CREATE TABLE IF NOT EXISTS opt_analytics_usage_ledger (
|
||||
usage_id TEXT PRIMARY KEY,
|
||||
tenant_id TEXT NOT NULL,
|
||||
project_id TEXT,
|
||||
principal_id TEXT NOT NULL,
|
||||
query_id TEXT NOT NULL,
|
||||
engine TEXT NOT NULL,
|
||||
rows_scanned BIGINT NOT NULL DEFAULT 0,
|
||||
bytes_scanned BIGINT NOT NULL DEFAULT 0,
|
||||
storage_read_bytes BIGINT NOT NULL DEFAULT 0,
|
||||
execution_ms BIGINT NOT NULL DEFAULT 0,
|
||||
cost_units NUMERIC(20, 6) NOT NULL,
|
||||
terminated_by_budget BOOLEAN NOT NULL DEFAULT false,
|
||||
recorded_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS opt_analytics_usage_ledger_window_idx
|
||||
ON opt_analytics_usage_ledger (tenant_id, recorded_at DESC);
|
||||
|
||||
DROP TRIGGER IF EXISTS opt_analytics_usage_ledger_immutable ON opt_analytics_usage_ledger;
|
||||
CREATE TRIGGER opt_analytics_usage_ledger_immutable
|
||||
BEFORE UPDATE OR DELETE ON opt_analytics_usage_ledger
|
||||
FOR EACH ROW EXECUTE FUNCTION opt_analytics_forbid_mutation();
|
||||
@@ -0,0 +1,349 @@
|
||||
/**
|
||||
* tests/analytics/analytics-governance-postgres.realinfra.test.ts
|
||||
* ================================================================
|
||||
* sql/009 against a REAL Postgres.
|
||||
*
|
||||
* A migration nobody ran is a migration that does not work, and a trigger
|
||||
* nobody fired is a comment. So this applies sql/008 and sql/009 into a
|
||||
* throwaway schema and then attempts, against the database, every write the
|
||||
* constraints exist to refuse:
|
||||
*
|
||||
* BI-P031 UPDATE a CERTIFIED metric's meaning in place -> must RAISE.
|
||||
* This is the layer that holds when something writes without going
|
||||
* through the application: a migration script, a psql session, an
|
||||
* admin tool. A TypeScript guard does not survive `psql`.
|
||||
* BI-P030 UPDATE a published semantic model version's content -> must RAISE.
|
||||
* Two ACTIVE versions of one model -> must violate a unique index.
|
||||
* BI-P028 A currency-formatted metric with no currency -> must be refused.
|
||||
* Certification without an actor or the two definitions -> refused.
|
||||
* BI-P018 A `compiled_query_hash` that is not a 64-hex digest -> refused.
|
||||
* BI-P021 UPDATE or DELETE a usage-ledger row -> must RAISE (append-only).
|
||||
*
|
||||
* Gated on ANALYTICS_TEST_DATABASE_URL, matching the convention in
|
||||
* analytics-domain-postgres.realinfra.test.ts. With it set and Postgres down,
|
||||
* this FAILS rather than skipping.
|
||||
*
|
||||
* docker run -d --name pg -p 25433:5432 -e POSTGRES_PASSWORD=test \
|
||||
* -e POSTGRES_DB=opt postgres:16-alpine
|
||||
* ANALYTICS_TEST_DATABASE_URL=postgres://postgres:test@127.0.0.1:25433/opt \
|
||||
* pnpm vitest run tests/analytics/analytics-governance-postgres.realinfra.test.ts
|
||||
*/
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
import postgres from 'postgres';
|
||||
|
||||
const TEST_DATABASE_URL = process.env.ANALYTICS_TEST_DATABASE_URL;
|
||||
const run = TEST_DATABASE_URL ? describe : describe.skip;
|
||||
const SCHEMA = `bi_p009_${Date.now().toString(36)}`;
|
||||
|
||||
let sql: ReturnType<typeof postgres>;
|
||||
|
||||
/** Did the statement RAISE? Returns the message, or undefined if it succeeded. */
|
||||
async function refusalOf(fn: () => Promise<unknown>): Promise<string | undefined> {
|
||||
try {
|
||||
await fn();
|
||||
return undefined;
|
||||
} catch (e) {
|
||||
return String((e as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
if (!TEST_DATABASE_URL) return;
|
||||
// The search_path is pinned on the CONNECTION rather than prefixed to every
|
||||
// statement. Prefixing makes each call a multi-statement simple query, and
|
||||
// postgres.js then returns the LAST statement's result — which silently
|
||||
// turned `SELECT state FROM ...` into the connection object in the first
|
||||
// version of this file, and made two assertions compare against nonsense.
|
||||
const admin = postgres(TEST_DATABASE_URL, { max: 1, onnotice: () => undefined });
|
||||
await admin.unsafe(`CREATE SCHEMA IF NOT EXISTS ${SCHEMA}`);
|
||||
await admin.end({ timeout: 5 });
|
||||
|
||||
sql = postgres(TEST_DATABASE_URL, {
|
||||
max: 2,
|
||||
onnotice: () => undefined,
|
||||
connection: { search_path: SCHEMA },
|
||||
});
|
||||
for (const file of [
|
||||
'sql/008_analytics_domain_model.sql',
|
||||
'sql/009_analytics_execution_metrics_governance.sql',
|
||||
]) {
|
||||
await sql.unsafe(fs.readFileSync(path.join(process.cwd(), file), 'utf8'));
|
||||
}
|
||||
|
||||
// A dataset and a metric to govern.
|
||||
await sql.unsafe(`
|
||||
INSERT INTO opt_analytics_dataset
|
||||
(dataset_id, tenant_id, project_id, name, tier, schema_contract_id, schema_contract_version,
|
||||
owner_id, source_ref, freshness_slo_seconds, retention_days, classification, status)
|
||||
VALUES ('ds_sales','t_alpha','p_main','sales','SERVING','c_sales','1.0.0',
|
||||
'u_1','0pt://sourcing/sales', 3600, 90, 'none', 'active')
|
||||
ON CONFLICT DO NOTHING;`);
|
||||
}, 120_000);
|
||||
|
||||
afterAll(async () => {
|
||||
if (!TEST_DATABASE_URL || !sql) return;
|
||||
await sql.unsafe(`DROP SCHEMA IF EXISTS ${SCHEMA} CASCADE`);
|
||||
await sql.end({ timeout: 5 });
|
||||
});
|
||||
|
||||
run('sql/009 applies, and replays', () => {
|
||||
it('is idempotent — a replay onto an already-migrated schema succeeds', async () => {
|
||||
const text = fs.readFileSync(
|
||||
path.join(process.cwd(), 'sql/009_analytics_execution_metrics_governance.sql'),
|
||||
'utf8',
|
||||
);
|
||||
// The restore drill replays every file in order onto a live database.
|
||||
await expect(sql.unsafe(text)).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it('added every BI-P018 column the execution record needs', async () => {
|
||||
const rows = await sql.unsafe(
|
||||
`SELECT column_name FROM information_schema.columns
|
||||
WHERE table_schema = '${SCHEMA}' AND table_name = 'opt_analytics_query_execution'`,
|
||||
);
|
||||
const names = new Set((rows as unknown as Array<{ column_name: string }>).map((r) => r.column_name));
|
||||
for (const col of [
|
||||
'principal_id',
|
||||
'workload',
|
||||
'compiled_query_hash',
|
||||
'datasets',
|
||||
'metrics',
|
||||
'cache_status',
|
||||
'cost_units',
|
||||
]) {
|
||||
expect(names.has(col), `missing ${col}`).toBe(true);
|
||||
}
|
||||
// And the one it must NOT have.
|
||||
expect(names.has('query_text')).toBe(false);
|
||||
expect(names.has('sql')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
run('BI-P031 — a CERTIFIED metric cannot be modified in place, enforced by the DATABASE', () => {
|
||||
const base = '';
|
||||
|
||||
beforeAll(async () => {
|
||||
await sql.unsafe(`${base}
|
||||
INSERT INTO opt_analytics_metric_definition
|
||||
(metric_id, tenant_id, project_id, name, dataset_id, aggregation, measure_field,
|
||||
allowed_dimension_ids, current_version, owner_id, description,
|
||||
metric_type, time_dimension, unit, currency, display_format, status, filters_json,
|
||||
certification, business_definition, technical_definition, meaning_hash,
|
||||
certified_at, certified_by)
|
||||
VALUES ('m_revenue','t_alpha','p_main','net_revenue','ds_sales','sum','amount_sek',
|
||||
ARRAY['country'], 3, 'finance@siax.io', 'Invoiced revenue net of credits',
|
||||
'simple','invoiced_at','SEK','SEK','currency','ACTIVE','[]',
|
||||
'certified','What we invoiced','sum(amount_sek)', repeat('a',64),
|
||||
now(), 'u_1')
|
||||
ON CONFLICT (metric_id) DO NOTHING;`);
|
||||
});
|
||||
|
||||
it('REFUSES an in-place change to the meaning hash', async () => {
|
||||
const message = await refusalOf(() =>
|
||||
sql.unsafe(`${base} UPDATE opt_analytics_metric_definition
|
||||
SET meaning_hash = repeat('b',64) WHERE metric_id = 'm_revenue'`),
|
||||
);
|
||||
expect(message, "a certified metric's meaning was changed in place, in the database").toBeDefined();
|
||||
expect(message).toContain('BI-P031');
|
||||
expect(message).toContain('mint version 4');
|
||||
});
|
||||
|
||||
it('ALLOWS an editorial change that leaves the meaning hash alone', async () => {
|
||||
await expect(
|
||||
sql.unsafe(`${base} UPDATE opt_analytics_metric_definition
|
||||
SET description = 'Clearer wording' WHERE metric_id = 'm_revenue'`),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it('ALLOWS a meaning change that comes with a version bump', async () => {
|
||||
await expect(
|
||||
sql.unsafe(`${base} UPDATE opt_analytics_metric_definition
|
||||
SET meaning_hash = repeat('c',64), current_version = 4
|
||||
WHERE metric_id = 'm_revenue'`),
|
||||
).resolves.toBeDefined();
|
||||
// Put it back for the remaining assertions.
|
||||
await sql.unsafe(`${base} UPDATE opt_analytics_metric_definition
|
||||
SET current_version = 3 WHERE metric_id = 'm_revenue'`);
|
||||
});
|
||||
|
||||
it('refuses a currency-formatted metric with no currency', async () => {
|
||||
const message = await refusalOf(() =>
|
||||
sql.unsafe(`${base} INSERT INTO opt_analytics_metric_definition
|
||||
(metric_id, tenant_id, project_id, name, dataset_id, aggregation, measure_field,
|
||||
owner_id, display_format, currency, time_dimension)
|
||||
VALUES ('m_bad','t_alpha','p_main','bad_metric','ds_sales','sum','x','u','currency',NULL,'t')`),
|
||||
);
|
||||
expect(message).toContain('currency_pairing');
|
||||
});
|
||||
|
||||
it('refuses a currency on a non-currency format', async () => {
|
||||
const message = await refusalOf(() =>
|
||||
sql.unsafe(`${base} INSERT INTO opt_analytics_metric_definition
|
||||
(metric_id, tenant_id, project_id, name, dataset_id, aggregation, measure_field,
|
||||
owner_id, display_format, currency, time_dimension)
|
||||
VALUES ('m_bad2','t_alpha','p_main','bad_metric2','ds_sales','sum','x','u','integer','SEK','t')`),
|
||||
);
|
||||
expect(message).toContain('currency_pairing');
|
||||
});
|
||||
|
||||
it('refuses certification without an actor, a time, and both definitions', async () => {
|
||||
const message = await refusalOf(() =>
|
||||
sql.unsafe(`${base} INSERT INTO opt_analytics_metric_definition
|
||||
(metric_id, tenant_id, project_id, name, dataset_id, aggregation, measure_field,
|
||||
owner_id, display_format, time_dimension, certification)
|
||||
VALUES ('m_bad3','t_alpha','p_main','bad_metric3','ds_sales','sum','x','u','decimal','t','certified')`),
|
||||
);
|
||||
expect(message).toContain('certification_act');
|
||||
});
|
||||
|
||||
it('refuses an unknown lifecycle status', async () => {
|
||||
const message = await refusalOf(() =>
|
||||
sql.unsafe(`${base} UPDATE opt_analytics_metric_definition
|
||||
SET status = 'PROBABLY_FINE' WHERE metric_id = 'm_revenue'`),
|
||||
);
|
||||
expect(message).toContain('metric_status');
|
||||
});
|
||||
});
|
||||
|
||||
run('BI-P030 — a published semantic model version is immutable, and only one is active', () => {
|
||||
const base = '';
|
||||
|
||||
beforeAll(async () => {
|
||||
for (const [id, version, status] of [
|
||||
['smv_1', 1, 'active'],
|
||||
['smv_2', 2, 'draft'],
|
||||
] as const) {
|
||||
await sql.unsafe(`${base}
|
||||
INSERT INTO opt_analytics_semantic_model_version
|
||||
(semantic_model_version_id, model_id, tenant_id, project_id, version,
|
||||
datasets, relationships_json, metrics_json, dimensions_json, status, hash)
|
||||
VALUES ('${id}','sm_sales','t_alpha','p_main',${version},
|
||||
ARRAY['ds_sales'],'[]','[]','[]','${status}', repeat('${version}',64))
|
||||
ON CONFLICT DO NOTHING;`);
|
||||
}
|
||||
});
|
||||
|
||||
it("REFUSES an update to a published version's content", async () => {
|
||||
const message = await refusalOf(() =>
|
||||
sql.unsafe(`${base} UPDATE opt_analytics_semantic_model_version
|
||||
SET metrics_json = '[{"id":"m_new"}]' WHERE semantic_model_version_id = 'smv_1'`),
|
||||
);
|
||||
expect(message, 'a pinned semantic model version was rewritten under a dashboard').toBeDefined();
|
||||
expect(message).toContain('BI-P030');
|
||||
});
|
||||
|
||||
it('ALLOWS a status transition, which is not a content change', async () => {
|
||||
await expect(
|
||||
sql.unsafe(`${base} UPDATE opt_analytics_semantic_model_version
|
||||
SET status = 'superseded' WHERE semantic_model_version_id = 'smv_1'`),
|
||||
).resolves.toBeDefined();
|
||||
await sql.unsafe(`${base} UPDATE opt_analytics_semantic_model_version
|
||||
SET status = 'active' WHERE semantic_model_version_id = 'smv_1'`);
|
||||
});
|
||||
|
||||
it('refuses TWO active versions of the same model — half the dashboards would read the other one', async () => {
|
||||
const message = await refusalOf(() =>
|
||||
sql.unsafe(`${base} UPDATE opt_analytics_semantic_model_version
|
||||
SET status = 'active' WHERE semantic_model_version_id = 'smv_2'`),
|
||||
);
|
||||
expect(message).toContain('opt_analytics_semantic_model_one_active');
|
||||
});
|
||||
|
||||
it('refuses a hash that is not a sha256 digest', async () => {
|
||||
const message = await refusalOf(() =>
|
||||
sql.unsafe(`${base}
|
||||
INSERT INTO opt_analytics_semantic_model_version
|
||||
(semantic_model_version_id, model_id, tenant_id, project_id, version,
|
||||
datasets, relationships_json, metrics_json, dimensions_json, status, hash)
|
||||
VALUES ('smv_bad','sm_sales','t_alpha','p_main',9,ARRAY['ds_sales'],'[]','[]','[]','draft','not-a-hash')`),
|
||||
);
|
||||
expect(message).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
run('BI-P018 / BI-P021 — the execution record and the usage ledger', () => {
|
||||
const base = '';
|
||||
|
||||
it('refuses a compiled_query_hash that is not a 64-hex digest', async () => {
|
||||
const message = await refusalOf(() =>
|
||||
sql.unsafe(`${base} INSERT INTO opt_analytics_query_execution
|
||||
(execution_id, tenant_id, project_id, engine_query_id, state, provider, compiled_query_hash)
|
||||
VALUES ('e_bad','t_alpha','p_main','q1','queued','clickhouse','SELECT * FROM sales')`),
|
||||
);
|
||||
expect(message).toContain('hash_shape');
|
||||
});
|
||||
|
||||
it('accepts a proper execution record', async () => {
|
||||
await expect(
|
||||
sql.unsafe(`${base} INSERT INTO opt_analytics_query_execution
|
||||
(execution_id, tenant_id, project_id, engine_query_id, state, provider,
|
||||
compiled_query_hash, principal_id, workload, cache_status, cost_units)
|
||||
VALUES ('e_ok','t_alpha','p_main','q1','succeeded','clickhouse',
|
||||
repeat('a',64),'u_1','dashboard','hit',0.5)`),
|
||||
).resolves.toBeDefined();
|
||||
});
|
||||
|
||||
it('refuses an unknown cache status and an unknown workload', async () => {
|
||||
expect(
|
||||
await refusalOf(() =>
|
||||
sql.unsafe(`${base} UPDATE opt_analytics_query_execution
|
||||
SET cache_status = 'probably' WHERE execution_id = 'e_ok'`),
|
||||
),
|
||||
).toContain('cache_status');
|
||||
expect(
|
||||
await refusalOf(() =>
|
||||
sql.unsafe(`${base} UPDATE opt_analytics_query_execution
|
||||
SET workload = 'vibes' WHERE execution_id = 'e_ok'`),
|
||||
),
|
||||
).toContain('workload');
|
||||
});
|
||||
|
||||
it('the usage ledger is APPEND-ONLY — update and delete both raise', async () => {
|
||||
await sql.unsafe(`${base} INSERT INTO opt_analytics_usage_ledger
|
||||
(usage_id, tenant_id, principal_id, query_id, engine, rows_scanned, bytes_scanned,
|
||||
execution_ms, cost_units, terminated_by_budget)
|
||||
VALUES ('u_1','t_alpha','p_1','q1','clickhouse',1000,2000,50,0.25,true)`);
|
||||
|
||||
expect(
|
||||
await refusalOf(() =>
|
||||
sql.unsafe(`${base} UPDATE opt_analytics_usage_ledger SET cost_units = 0 WHERE usage_id = 'u_1'`),
|
||||
),
|
||||
'a billing row was editable',
|
||||
).toBeDefined();
|
||||
expect(
|
||||
await refusalOf(() =>
|
||||
sql.unsafe(`${base} DELETE FROM opt_analytics_usage_ledger WHERE usage_id = 'u_1'`),
|
||||
),
|
||||
'a billing row was deletable',
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it('an analyst statement refusal must carry a reason', async () => {
|
||||
const message = await refusalOf(() =>
|
||||
sql.unsafe(`${base} INSERT INTO opt_analytics_sql_statement
|
||||
(statement_id, tenant_id, principal_id, statement_normalized, statement_hash, outcome)
|
||||
VALUES ('s_bad','t_alpha','u_1','SELECT ?', repeat('a',64), 'refused')`),
|
||||
);
|
||||
expect(message, 'a refusal with no reason was accepted — an unreviewable audit trail').toBeDefined();
|
||||
});
|
||||
|
||||
it('a freshness row must carry a disclosure a UI can render', async () => {
|
||||
const message = await refusalOf(() =>
|
||||
sql.unsafe(`${base} INSERT INTO opt_analytics_dataset_freshness
|
||||
(tenant_id, dataset_id, state, expected_interval_ms, disclosure)
|
||||
VALUES ('t_alpha','ds_sales','STALE',3600000,' ')`),
|
||||
);
|
||||
expect(message).toBeDefined();
|
||||
});
|
||||
|
||||
it('freshness defaults to UNKNOWN, never to FRESH', async () => {
|
||||
await sql.unsafe(`${base} INSERT INTO opt_analytics_dataset_freshness
|
||||
(tenant_id, dataset_id, expected_interval_ms, disclosure)
|
||||
VALUES ('t_beta','ds_sales',3600000,'not yet computed')`);
|
||||
const rows = await sql.unsafe(`${base} SELECT state FROM opt_analytics_dataset_freshness
|
||||
WHERE tenant_id = 't_beta'`);
|
||||
expect((rows as unknown as Array<{ state: string }>)[0].state).toBe('UNKNOWN');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user