Files
0pt/sql/022_canonical_facts.sql
SIAX Agent 0213459f33
masterplan-lock / check (push) Successful in 5s
CI (SIAX Cloud) / sonar (pull_request) Skipped
masterplan-lock / check (pull_request) Successful in 4s
CI (SIAX Cloud) / Component conformance (blocking) (pull_request) Successful in 15s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 21s
CI (SIAX Cloud) / security (pull_request) Successful in 48s
CI (SIAX Cloud) / quality (pull_request) Successful in 1m47s
CI (SIAX Cloud) / integration (pull_request) Successful in 10m39s
fix(sql): 022 canonical facts blir idempotent (DROP POLICY IF EXISTS)
CREATE POLICY saknade skydd mot omkörning, så en retry efter partiell körning
skulle fastna på 'policy already exists'. Tabellerna använder redan
CREATE TABLE/INDEX IF NOT EXISTS; policynivån görs nu likvärdig. Rubriken
rättad från 020 till 022.

Verifierat på scratch-databas: två körningar i rad OK, RLS ENABLE+FORCE,
3 policies, 11 index, cross-tenant INSERT nekad och tenant B ser 0 rader.
2026-11-07 17:24:56 +01:00

103 lines
5.2 KiB
SQL

-- 022_canonical_facts.sql — 0pt canonical structured observations (X-800 / Scrape).
--
-- 0pt owns normalized OBSERVED facts (what a source said, when, from which
-- artifact). L0RE owns epistemic propositions/assertions and projections; it
-- must consume these rows through this API/contract, never by writing here.
--
-- Conflict preservation is structural: the unique key includes source_ref and
-- observed_at, so two sources disagreeing about the same predicate are two
-- rows, never an overwrite. `status` records observed/supported/disputed/
-- retracted; reconciliation is a separate, explicit decision.
--
-- Tenant isolation: RLS is FORCEd and keyed on a transaction-local GUC that
-- only trusted server code sets (same pattern as 019_document_credit_rls.sql).
CREATE TABLE IF NOT EXISTS opt_canonical_entities (
entity_id UUID PRIMARY KEY,
tenant_id TEXT NOT NULL,
entity_type TEXT NOT NULL, -- company|person|web_page|product|...
identity_namespace TEXT NOT NULL, -- url|orgnr|lei|gtin|...
identity_value TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (tenant_id, entity_type, identity_namespace, identity_value)
);
CREATE INDEX IF NOT EXISTS idx_canonical_entities_tenant_type
ON opt_canonical_entities (tenant_id, entity_type);
CREATE TABLE IF NOT EXISTS opt_canonical_facts (
fact_id UUID PRIMARY KEY,
tenant_id TEXT NOT NULL,
entity_id UUID NOT NULL REFERENCES opt_canonical_entities (entity_id),
predicate TEXT NOT NULL, -- e.g. web.title, product.price
value_json JSONB NOT NULL,
value_type TEXT NOT NULL, -- string|number|boolean|date|json
value_unit TEXT,
source_ref TEXT NOT NULL,
source_record_id TEXT,
st0re_artifact_id TEXT,
aud0_evidence_ref TEXT,
run_ref TEXT,
extractor_version TEXT NOT NULL,
observed_at TIMESTAMPTZ NOT NULL,
valid_from TIMESTAMPTZ,
valid_to TIMESTAMPTZ,
confidence NUMERIC CHECK (confidence BETWEEN 0 AND 1),
quality_status TEXT NOT NULL DEFAULT 'unvalidated', -- passed|warned|failed|unvalidated
schema_version TEXT NOT NULL DEFAULT '0pt.canonical_fact/v1',
status TEXT NOT NULL DEFAULT 'observed'
CHECK (status IN ('observed','supported','disputed','retracted')),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (tenant_id, entity_id, predicate, source_ref, observed_at)
);
CREATE INDEX IF NOT EXISTS idx_canonical_facts_entity
ON opt_canonical_facts (tenant_id, entity_id, predicate);
CREATE INDEX IF NOT EXISTS idx_canonical_facts_predicate
ON opt_canonical_facts (tenant_id, predicate, observed_at DESC);
CREATE TABLE IF NOT EXISTS opt_canonical_relations (
relation_id UUID PRIMARY KEY,
tenant_id TEXT NOT NULL,
subject_id UUID NOT NULL REFERENCES opt_canonical_entities (entity_id),
predicate TEXT NOT NULL, -- LINKS_TO|SOLD_BY|USES_SOFTWARE|...
object_id UUID NOT NULL REFERENCES opt_canonical_entities (entity_id),
attributes_json JSONB NOT NULL DEFAULT '{}'::jsonb,
source_ref TEXT NOT NULL,
st0re_artifact_id TEXT,
aud0_evidence_ref TEXT,
run_ref TEXT,
extractor_version TEXT NOT NULL,
observed_at TIMESTAMPTZ NOT NULL,
valid_from TIMESTAMPTZ,
valid_to TIMESTAMPTZ,
confidence NUMERIC CHECK (confidence BETWEEN 0 AND 1),
schema_version TEXT NOT NULL DEFAULT '0pt.canonical_relation/v1',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (tenant_id, subject_id, predicate, object_id, source_ref, observed_at)
);
CREATE INDEX IF NOT EXISTS idx_canonical_relations_subject
ON opt_canonical_relations (tenant_id, subject_id, predicate);
CREATE INDEX IF NOT EXISTS idx_canonical_relations_object
ON opt_canonical_relations (tenant_id, object_id, predicate);
ALTER TABLE opt_canonical_entities ENABLE ROW LEVEL SECURITY;
ALTER TABLE opt_canonical_entities FORCE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS opt_canonical_entities_tenant ON opt_canonical_entities;
CREATE POLICY opt_canonical_entities_tenant ON opt_canonical_entities
USING (tenant_id = current_setting('app.opt_canonical_tenant_id', true))
WITH CHECK (tenant_id = current_setting('app.opt_canonical_tenant_id', true));
ALTER TABLE opt_canonical_facts ENABLE ROW LEVEL SECURITY;
ALTER TABLE opt_canonical_facts FORCE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS opt_canonical_facts_tenant ON opt_canonical_facts;
CREATE POLICY opt_canonical_facts_tenant ON opt_canonical_facts
USING (tenant_id = current_setting('app.opt_canonical_tenant_id', true))
WITH CHECK (tenant_id = current_setting('app.opt_canonical_tenant_id', true));
ALTER TABLE opt_canonical_relations ENABLE ROW LEVEL SECURITY;
ALTER TABLE opt_canonical_relations FORCE ROW LEVEL SECURITY;
DROP POLICY IF EXISTS opt_canonical_relations_tenant ON opt_canonical_relations;
CREATE POLICY opt_canonical_relations_tenant ON opt_canonical_relations
USING (tenant_id = current_setting('app.opt_canonical_tenant_id', true))
WITH CHECK (tenant_id = current_setting('app.opt_canonical_tenant_id', true));