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>
347 lines
19 KiB
PL/PgSQL
347 lines
19 KiB
PL/PgSQL
-- 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();
|