CI (SIAX Cloud) / security (push) Successful in 41s
Security Scan (SIAX Cloud) / security (push) Successful in 40s
CI (SIAX Cloud) / integration (push) Successful in 52s
CI (SIAX Cloud) / quality (push) Successful in 1m5s
CI (SIAX Cloud) / security (pull_request) Successful in 31s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 29s
CI (SIAX Cloud) / integration (pull_request) Successful in 45s
CI (SIAX Cloud) / quality (pull_request) Successful in 53s
Implements the eight BI program prompts that make 0pt the canonical SIAX
analytics capability. Every file is NEW: not one existing file is modified,
and src/analytics/clickhouse-analytics.ts is byte-identical to main.
BI-P002 — the nineteen canonical entities, verbatim, with tenant_id +
project_id on every tenant-owned record. sql/008 gives each one a table.
The four version entities BI-P002 names as immutable are enforced by BEFORE
UPDATE/DELETE triggers, not by a TypeScript `readonly` — proven by direct SQL
against a live Postgres.
BI-P003 — RAW/NORMALIZED/CURATED/SERVING with the read rule enforced:
assertReadableByBiSurface refuses a RAW dataset to a dashboard unless a
recorded exception exists. All eight required declarations are NOT NULL.
BI-P004 — DatasetContract and a fail-closed compatibility gate. Fifteen
breaking-change classes are each tested twice: refused under a minor bump,
accepted under a major one.
BI-P005 — Arrow IPC alongside JSON, with a hand-written FlatBuffers writer so
no npm dependency (or CVE surface) is added. Conformance comes from three
independent implementations, not from self-agreement: pyarrow 25.0.1 and
DuckDB 1.5.5 read identical values from both ClickHouse's Arrow and ours, and
ClickHouse's own Arrow C++ ingests our 20,000-row batch with exact aggregates.
BI-P009 — SiaxAnalyticsEngine with all eight capabilities. The existing
ClickHouseAnalytics is COMPOSED, never replaced. executeQuery (analyst SQL) is
declared and deliberately refused with CAPABILITY_UNAVAILABLE until BI-P017 and
BI-P033 land, rather than opening the raw-SQL hole BI-P011 enumerates.
BI-P010 — four purpose-built tables, not one giant one, and the difference is
measured against the counterfactual: 48.8ms/40,960 rows vs 174.4ms/5,400,000
rows for the same question; 5/660 granules vs 660/1149.
BI-P011 — layered isolation, each layer proven with the layer above removed.
Without the forced predicate the same statement returns 50,050 (both tenants);
with it, t_alpha gets 50. Without a row policy an unrestricted user sees 2
tenants; the restricted user running the IDENTICAL statement sees 1.
BI-P012 — every named limit, fired individually and identified by ClickHouse
error code, plus the half usually skipped: an unrelated tenant is timed while
a real CPU bomb is in flight and must still return the CORRECT answer.
Three defects found by writing tests that can fail:
* `CHECK (array_length(purposes,1) >= 1)` can NEVER fail — array_length is
NULL for '{}' and a NULL CHECK passes. Fixed with coalesce(); the test
that caught it is pinned.
* EXPLAIN ESTIMATE run under the caller's own max_rows_to_read fails with
code 158 exactly when the estimate would be over budget. Planner calls now
use a narrower envelope.
* result_overflow_mode=break returns HTTP 200 with 65,409 of 100,000 rows
and no indication it is short. Every SELECT ships `throw`.
Verified rather than assumed: server7's ClickHouse holds `opt_scraped` (51
tables, 407,793,471 rows, `persons`/`cz_owners` included) — the b0ts plane this
module is prohibited from touching — has no `opt_analytics` database, zero
tenant_id columns and zero row policies. Nothing there was modified; every
proof ran against disposable containers.
Evidence: evidence/bi-p002-p012/{test-transcript.txt,bi-p010-benchmark.json,
bi-p005-arrow-roundtrip.json,server7-clickhouse-probe.md}
Docs: docs/analytics/BI_ANALYTICS_CORE.md
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
365 lines
15 KiB
TypeScript
365 lines
15 KiB
TypeScript
/**
|
|
* BI-P002 DoD: "Database migrations + persistence + tenant isolation +
|
|
* restore tests."
|
|
*
|
|
* REAL Postgres, same honest-degrade convention as
|
|
* tests/p0/source-registry-postgres.realinfra.test.ts: gated on
|
|
* ANALYTICS_TEST_DATABASE_URL, SKIPS HONESTLY when unset, never fabricates.
|
|
*
|
|
* docker run -d --name opt-bi-pg -p 15455:5432 \
|
|
* -e POSTGRES_USER=opt -e POSTGRES_PASSWORD=opt_bi_throwaway \
|
|
* -e POSTGRES_DB=opt_bi_test postgres:16-alpine
|
|
* ANALYTICS_TEST_DATABASE_URL=postgres://opt:opt_bi_throwaway@localhost:15455/opt_bi_test \
|
|
* pnpm vitest run tests/analytics/analytics-domain-postgres.realinfra.test.ts
|
|
*
|
|
* The RESTORE half is the part that catches migration-chain rot: it replays
|
|
* sql/001..008 in order into a FRESH database, exactly as
|
|
* docs/runbook/backup-restore.md describes, and only then runs the
|
|
* persistence assertions. sql/003_datasets.sql exists because that same drill
|
|
* found a table no file ever created; this suite keeps that class of defect
|
|
* from coming back through sql/008.
|
|
*/
|
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import postgres from "postgres";
|
|
import {
|
|
ANALYTICS_DOMAIN_ENTITIES,
|
|
ANALYTICS_ENTITY_TABLES,
|
|
IMMUTABLE_VERSION_ENTITIES,
|
|
} from "@/src/analytics/domain/entities";
|
|
import { AnalyticsDomainRepository } from "@/src/analytics/repositories/analytics-domain-repository";
|
|
import { contractHash, type DatasetContract } from "@/src/analytics/domain/contract";
|
|
|
|
const TEST_DATABASE_URL = process.env.ANALYTICS_TEST_DATABASE_URL;
|
|
const run = TEST_DATABASE_URL ? describe : describe.skip;
|
|
|
|
/** A schema of its own so the drill starts from genuinely nothing. */
|
|
const SCHEMA = `bi_p002_${Date.now().toString(36)}`;
|
|
|
|
let sql: ReturnType<typeof postgres> | undefined;
|
|
let reachable = false;
|
|
let unreachableReason = "";
|
|
/** Which sql/*.sql files the restore drill actually applied. */
|
|
const applied: string[] = [];
|
|
|
|
const ALPHA = { tenant_id: "t_alpha", project_id: "p_main" };
|
|
const BETA = { tenant_id: "t_beta", project_id: "p_main" };
|
|
|
|
function repo(): AnalyticsDomainRepository {
|
|
return new AnalyticsDomainRepository(sql!);
|
|
}
|
|
|
|
function contract(datasetId: string): DatasetContract {
|
|
return {
|
|
dataset_id: datasetId,
|
|
version: "1.0.0",
|
|
tier: "CURATED",
|
|
fields: [{ name: "id", type: "string", nullable: false, role: "identifier", primary_identifier: true }],
|
|
primary_identifiers: ["id"],
|
|
dimensions: [],
|
|
measures: [],
|
|
freshness_slo_seconds: 3600,
|
|
retention_days: 365,
|
|
pii_classification: "none",
|
|
source_lineage: ["src_x"],
|
|
quality_rules: [],
|
|
allowed_purposes: ["analytics"],
|
|
};
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
if (!TEST_DATABASE_URL) {
|
|
console.warn("[realinfra] ANALYTICS_TEST_DATABASE_URL not set — skipping honestly, not fabricating success.");
|
|
return;
|
|
}
|
|
try {
|
|
sql = postgres(TEST_DATABASE_URL, { prepare: false, ssl: false, max: 3 });
|
|
await Promise.race([
|
|
sql`SELECT 1`,
|
|
new Promise((_, reject) => setTimeout(() => reject(new Error("connect timeout")), 5000)),
|
|
]);
|
|
// RESTORE DRILL: a brand-new schema, then every migration in order.
|
|
await sql.unsafe(`CREATE SCHEMA ${SCHEMA}`);
|
|
await sql.unsafe(`SET search_path TO ${SCHEMA}, public`);
|
|
const dir = path.join(process.cwd(), "sql");
|
|
for (const file of fs.readdirSync(dir).filter((f) => f.endsWith(".sql")).sort()) {
|
|
const body = fs.readFileSync(path.join(dir, file), "utf8");
|
|
try {
|
|
await sql.unsafe(`SET search_path TO ${SCHEMA}, public; ${body}`);
|
|
applied.push(file);
|
|
} catch (err) {
|
|
// 001 needs the `vector` extension, which a stock postgres:16-alpine
|
|
// does not carry. Record the skip rather than hiding it — the files
|
|
// sql/008 actually depends on must still apply.
|
|
applied.push(`${file} (FAILED: ${String(err).slice(0, 120)})`);
|
|
}
|
|
}
|
|
reachable = true;
|
|
} catch (err) {
|
|
reachable = false;
|
|
unreachableReason = err instanceof Error ? err.message : String(err);
|
|
}
|
|
}, 60_000);
|
|
|
|
afterAll(async () => {
|
|
if (sql && reachable) {
|
|
await sql.unsafe(`DROP SCHEMA IF EXISTS ${SCHEMA} CASCADE`).catch(() => undefined);
|
|
}
|
|
await sql?.end({ timeout: 5 });
|
|
});
|
|
|
|
run("BI-P002 analytics domain model against a LIVE Postgres", () => {
|
|
it("restore drill: sql/008 applies cleanly on top of the replayed chain", (ctx) => {
|
|
if (!reachable) ctx.skip();
|
|
expect(unreachableReason).toBe("");
|
|
const p008 = applied.find((f) => f.startsWith("008_"));
|
|
expect(p008, `sql/008 was not applied. Applied: ${applied.join(", ")}`).toBe(
|
|
"008_analytics_domain_model.sql",
|
|
);
|
|
});
|
|
|
|
it("all nineteen entity tables really exist after the restore", async (ctx) => {
|
|
if (!reachable) ctx.skip();
|
|
const rows = await sql!<{ table_name: string }[]>`
|
|
SELECT table_name FROM information_schema.tables WHERE table_schema = ${SCHEMA}`;
|
|
const present = new Set(rows.map((r) => r.table_name));
|
|
const missing = ANALYTICS_DOMAIN_ENTITIES.filter((e) => !present.has(ANALYTICS_ENTITY_TABLES[e]));
|
|
expect(missing).toEqual([]);
|
|
});
|
|
|
|
it("replaying sql/008 a second time is a no-op, not an error", async (ctx) => {
|
|
if (!reachable) ctx.skip();
|
|
const body = fs.readFileSync(path.join(process.cwd(), "sql", "008_analytics_domain_model.sql"), "utf8");
|
|
await expect(sql!.unsafe(`SET search_path TO ${SCHEMA}, public; ${body}`)).resolves.toBeDefined();
|
|
});
|
|
|
|
it("persists a dataset and reads it back through the repository", async (ctx) => {
|
|
if (!reachable) ctx.skip();
|
|
const r = repo();
|
|
const created = await r.createDataset(ALPHA, {
|
|
dataset_id: "ds_alpha_1",
|
|
name: "Alpha listings",
|
|
tier: "CURATED",
|
|
schema_contract_id: "dc_alpha",
|
|
schema_contract_version: "1.0.0",
|
|
owner_id: "team_data",
|
|
source_ref: "src_feed",
|
|
freshness_slo_seconds: 3600,
|
|
quality_rule_ids: [],
|
|
retention_days: 365,
|
|
classification: "none",
|
|
status: "active",
|
|
created_by: "usr_1",
|
|
});
|
|
expect(created.tenant_id).toBe("t_alpha");
|
|
const read = await r.getDataset(ALPHA, "ds_alpha_1");
|
|
expect(read?.name).toBe("Alpha listings");
|
|
expect(read?.freshness_slo_seconds).toBe(3600);
|
|
// Cross-check with direct SQL — proving the row is really in the table,
|
|
// not an artifact of the repository's own bookkeeping.
|
|
const direct = await sql!<{ c: string }[]>`
|
|
SELECT count(*)::text AS c FROM opt_analytics_dataset WHERE dataset_id = 'ds_alpha_1'`;
|
|
expect(Number(direct[0].c)).toBe(1);
|
|
});
|
|
|
|
it("TENANT ISOLATION: t_beta cannot read t_alpha's dataset, even with the exact id", async (ctx) => {
|
|
if (!reachable) ctx.skip();
|
|
const r = repo();
|
|
expect(await r.getDataset(BETA, "ds_alpha_1")).toBeUndefined();
|
|
expect(await r.listDatasets(BETA)).toEqual([]);
|
|
// And the row IS there — the empty result is scoping, not absence.
|
|
const direct = await sql!<{ c: string }[]>`SELECT count(*)::text AS c FROM opt_analytics_dataset`;
|
|
expect(Number(direct[0].c)).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it("TENANT ISOLATION: a version cannot be minted against another tenant's dataset", async (ctx) => {
|
|
if (!reachable) ctx.skip();
|
|
await expect(
|
|
repo().createDatasetVersion(BETA, {
|
|
dataset_id: "ds_alpha_1",
|
|
contract_hash: "deadbeef",
|
|
schema_contract_version: "1.0.0",
|
|
}),
|
|
).rejects.toMatchObject({ code: "NOT_FOUND" });
|
|
});
|
|
|
|
it("dataset versions are monotonic and carry the contract hash", async (ctx) => {
|
|
if (!reachable) ctx.skip();
|
|
const r = repo();
|
|
const c = contract("ds_alpha_1");
|
|
const v1 = await r.createDatasetVersion(ALPHA, {
|
|
dataset_id: "ds_alpha_1",
|
|
contract_hash: contractHash(c),
|
|
schema_contract_version: "1.0.0",
|
|
});
|
|
const v2 = await r.createDatasetVersion(ALPHA, {
|
|
dataset_id: "ds_alpha_1",
|
|
contract_hash: contractHash({ ...c, version: "2.0.0" }),
|
|
schema_contract_version: "2.0.0",
|
|
});
|
|
expect(v1.version).toBe(1);
|
|
expect(v2.version).toBe(2);
|
|
expect(v2.contract_hash).not.toBe(v1.contract_hash);
|
|
expect((await r.listDatasetVersions(ALPHA, "ds_alpha_1")).map((v) => v.version)).toEqual([1, 2]);
|
|
});
|
|
|
|
it("IMMUTABILITY is enforced by the DATABASE: UPDATE and DELETE on a version row are refused", async (ctx) => {
|
|
if (!reachable) ctx.skip();
|
|
// Direct SQL, bypassing every TypeScript `readonly` — the only test that
|
|
// proves the guarantee survives a psql session.
|
|
await expect(
|
|
sql!.unsafe(
|
|
`UPDATE ${SCHEMA}.opt_analytics_dataset_version SET contract_hash = 'tampered' WHERE dataset_id = 'ds_alpha_1'`,
|
|
),
|
|
).rejects.toThrow(/immutable/i);
|
|
await expect(
|
|
sql!.unsafe(`DELETE FROM ${SCHEMA}.opt_analytics_dataset_version WHERE dataset_id = 'ds_alpha_1'`),
|
|
).rejects.toThrow(/immutable/i);
|
|
// The rows are untouched.
|
|
const rows = await sql!<{ contract_hash: string }[]>`
|
|
SELECT contract_hash FROM opt_analytics_dataset_version WHERE dataset_id = 'ds_alpha_1'`;
|
|
expect(rows).toHaveLength(2);
|
|
for (const r of rows) expect(r.contract_hash).not.toBe("tampered");
|
|
});
|
|
|
|
it("all four immutable entities refuse an UPDATE, not just the one that was tested first", async (ctx) => {
|
|
if (!reachable) ctx.skip();
|
|
const r = repo();
|
|
await r.createDataset(ALPHA, {
|
|
dataset_id: "ds_imm",
|
|
name: "Immutability fixture",
|
|
tier: "SERVING",
|
|
schema_contract_id: "dc_i",
|
|
schema_contract_version: "1.0.0",
|
|
owner_id: "team_data",
|
|
source_ref: "src",
|
|
freshness_slo_seconds: 60,
|
|
quality_rule_ids: [],
|
|
retention_days: 30,
|
|
classification: "none",
|
|
status: "active",
|
|
});
|
|
const metric = await r.createMetric(ALPHA, {
|
|
metric_id: "m_imm",
|
|
name: "immutable metric",
|
|
dataset_id: "ds_imm",
|
|
aggregation: "sum",
|
|
measure_field: "value",
|
|
allowed_dimension_ids: [],
|
|
owner_id: "team_data",
|
|
});
|
|
await r.createMetricVersion(ALPHA, { metric_id: metric.metric_id, definition_json: '{"v":1}' });
|
|
await r.createDatasetVersion(ALPHA, {
|
|
dataset_id: "ds_imm",
|
|
contract_hash: "h1",
|
|
schema_contract_version: "1.0.0",
|
|
});
|
|
await sql!.unsafe(`INSERT INTO ${SCHEMA}.opt_analytics_semantic_model
|
|
(semantic_model_id, tenant_id, project_id, name, version, model_json, model_hash, owner_id)
|
|
VALUES ('sm_1','t_alpha','p_main','model_a',1,'{}','h','team_data')`);
|
|
await sql!.unsafe(`INSERT INTO ${SCHEMA}.opt_analytics_dashboard_definition
|
|
(dashboard_id, tenant_id, project_id, name, owner_id) VALUES ('db_1','t_alpha','p_main','dash','team_data')`);
|
|
await sql!.unsafe(`INSERT INTO ${SCHEMA}.opt_analytics_dashboard_version
|
|
(dashboard_version_id, dashboard_id, tenant_id, project_id, version, layout_json, layout_hash)
|
|
VALUES ('dv_1','db_1','t_alpha','p_main',1,'{}','h')`);
|
|
|
|
const updates: Record<string, string> = {
|
|
DatasetVersion: `UPDATE ${SCHEMA}.opt_analytics_dataset_version SET contract_hash='x' WHERE dataset_id='ds_imm'`,
|
|
MetricVersion: `UPDATE ${SCHEMA}.opt_analytics_metric_version SET definition_json='{}' WHERE metric_id='m_imm'`,
|
|
SemanticModel: `UPDATE ${SCHEMA}.opt_analytics_semantic_model SET model_json='{}' WHERE semantic_model_id='sm_1'`,
|
|
DashboardVersion: `UPDATE ${SCHEMA}.opt_analytics_dashboard_version SET layout_json='{}' WHERE dashboard_version_id='dv_1'`,
|
|
};
|
|
for (const entity of IMMUTABLE_VERSION_ENTITIES) {
|
|
await expect(sql!.unsafe(updates[entity]), `${entity} is NOT immutable`).rejects.toThrow(/immutable/i);
|
|
}
|
|
});
|
|
|
|
it("a metric version after the first must state WHY the number changed meaning", async (ctx) => {
|
|
if (!reachable) ctx.skip();
|
|
const r = repo();
|
|
await expect(
|
|
r.createMetricVersion(ALPHA, { metric_id: "m_imm", definition_json: '{"v":2}' }),
|
|
).rejects.toThrow();
|
|
const v2 = await r.createMetricVersion(ALPHA, {
|
|
metric_id: "m_imm",
|
|
definition_json: '{"v":2}',
|
|
change_reason: "excluded internal test accounts from the numerator",
|
|
});
|
|
expect(v2.version).toBe(2);
|
|
});
|
|
|
|
it("a DatasetGrant without a purpose is refused by the DATABASE (purpose limitation)", async (ctx) => {
|
|
if (!reachable) ctx.skip();
|
|
// Regression pin: the first version of this constraint was written
|
|
// `array_length(allowed_purposes, 1) >= 1`, which is NULL for '{}' — and a
|
|
// CHECK evaluating to NULL PASSES in Postgres. It could never fail. This
|
|
// test found that; keep it.
|
|
await expect(
|
|
sql!.unsafe(`INSERT INTO ${SCHEMA}.opt_analytics_dataset_grant
|
|
(grant_id, dataset_id, tenant_id, project_id, subject_id, subject_type, level, allowed_purposes)
|
|
VALUES ('g_1','ds_imm','t_alpha','p_main','usr_1','user','read', '{}')`),
|
|
).rejects.toThrow(/purposes_nonempty/);
|
|
// A grant WITH a purpose is accepted, so the constraint is not simply
|
|
// rejecting everything.
|
|
await expect(
|
|
sql!.unsafe(`INSERT INTO ${SCHEMA}.opt_analytics_dataset_grant
|
|
(grant_id, dataset_id, tenant_id, project_id, subject_id, subject_type, level, allowed_purposes)
|
|
VALUES ('g_ok','ds_imm','t_alpha','p_main','usr_1','user','read', ARRAY['market_analytics'])`),
|
|
).resolves.toBeDefined();
|
|
});
|
|
|
|
it("a BIApplicationBinding without a purpose is refused too, and carries no credential column", async (ctx) => {
|
|
if (!reachable) ctx.skip();
|
|
await expect(
|
|
sql!.unsafe(`INSERT INTO ${SCHEMA}.opt_analytics_bi_application_binding
|
|
(binding_id, tenant_id, project_id, kind, name, principal_id, allowed_purposes)
|
|
VALUES ('b_1','t_alpha','p_main','superset','sup','svc_1','{}')`),
|
|
).rejects.toThrow(/purposes_nonempty/);
|
|
|
|
const cols = await sql!<{ column_name: string }[]>`
|
|
SELECT column_name FROM information_schema.columns
|
|
WHERE table_schema = ${SCHEMA} AND table_name = 'opt_analytics_bi_application_binding'`;
|
|
const names = cols.map((c) => c.column_name.toLowerCase());
|
|
for (const forbidden of ["password", "secret", "dsn", "token", "connection_string", "clickhouse_url"]) {
|
|
expect(names, `BI binding must never store ${forbidden}`).not.toContain(forbidden);
|
|
}
|
|
});
|
|
|
|
it("the dimension catalog is tenant-scoped and FAILS CLOSED for a tenant with none", async (ctx) => {
|
|
if (!reachable) ctx.skip();
|
|
const r = repo();
|
|
await r.createDimension(ALPHA, {
|
|
dimension_id: "dim_1",
|
|
name: "make",
|
|
dataset_id: "ds_imm",
|
|
field_name: "vehicle_make",
|
|
field_type: "string",
|
|
classification: "none",
|
|
});
|
|
expect(await r.dimensionCatalog(ALPHA)).toEqual({ make: "vehicle_make" });
|
|
// t_beta declared nothing, so it may reference nothing.
|
|
expect(await r.dimensionCatalog(BETA)).toEqual({});
|
|
});
|
|
|
|
it("a dimension whose physical field is not identifier-safe is refused by the DATABASE", async (ctx) => {
|
|
if (!reachable) ctx.skip();
|
|
await expect(
|
|
sql!.unsafe(`INSERT INTO ${SCHEMA}.opt_analytics_dimension_definition
|
|
(dimension_id, tenant_id, project_id, name, dataset_id, field_name, field_type)
|
|
VALUES ('dim_bad','t_alpha','p_main','evil','ds_imm', 'x''; DROP TABLE opt_analytics_dataset; --','string')`),
|
|
).rejects.toThrow();
|
|
});
|
|
|
|
it("the repository refuses to act without a verified tenant scope", async (ctx) => {
|
|
if (!reachable) ctx.skip();
|
|
const r = repo();
|
|
await expect(r.getDataset({ tenant_id: "", project_id: "p" }, "ds_imm")).rejects.toMatchObject({
|
|
code: "UNAUTHENTICATED",
|
|
});
|
|
await expect(r.listDatasets({ tenant_id: "t_alpha", project_id: " " })).rejects.toMatchObject({
|
|
code: "UNAUTHENTICATED",
|
|
});
|
|
});
|
|
});
|