Files
Investidor-V2/migrations/004_mne_compatibility.sql
T
nala 3dc7c5c649
tests / ci (push) Canceled after 0s
commit 30/9
2026-09-30 09:05:43 +02:00

119 lines
7.3 KiB
PL/PgSQL

BEGIN;
SET LOCAL lock_timeout = '10s';
SET LOCAL search_path = pg_catalog, public;
SELECT pg_advisory_xact_lock(290926,170004);
-- A pre-existing bridge must match the schema fingerprint recorded on its first application.
DO $existing$
DECLARE name text; relation regclass; signature text;
BEGIN
FOREACH name IN ARRAY ARRAY['identity_links','sector_links','business_links','compatibility_checkpoints','compatibility_rejections','compatibility_controls'] LOOP
relation := to_regclass('mne.'||name);
IF relation IS NOT NULL THEN
SELECT md5(COALESCE((SELECT string_agg(a.attname||':'||format_type(a.atttypid,a.atttypmod)||':'||a.attnotnull::text||':'||COALESCE(pg_get_expr(d.adbin,d.adrelid),''),'|' ORDER BY a.attnum)
FROM pg_attribute a LEFT JOIN pg_attrdef d ON d.adrelid=a.attrelid AND d.adnum=a.attnum WHERE a.attrelid=relation AND a.attnum>0 AND NOT a.attisdropped),'')||COALESCE((SELECT string_agg(conname||':'||pg_get_constraintdef(oid),'|' ORDER BY conname) FROM pg_constraint WHERE conrelid=relation),'')) INTO signature;
IF obj_description(relation,'pg_class') IS DISTINCT FROM '004_mne_compatibility:'||signature THEN
RAISE EXCEPTION 'Unexpected compatibility table definition: %',name;
END IF;
END IF;
END LOOP;
END $existing$;
DO $preflight$
DECLARE item record;
BEGIN
FOR item IN SELECT * FROM (VALUES
('public.users','id','bigint'),('public.sectors','id','bigint'),
('public.existing_businesses','id','uuid'),('mne.parties','id','uuid'),
('mne.parties','user_id','uuid'),('mne.existing_businesses','id','uuid')
) AS expected(table_name,column_name,type_name) LOOP
IF NOT EXISTS (SELECT 1 FROM pg_attribute WHERE attrelid=to_regclass(item.table_name)
AND attname=item.column_name AND NOT attisdropped AND format_type(atttypid,atttypmod)=item.type_name) THEN
RAISE EXCEPTION 'Unexpected compatibility prerequisite: %.%',item.table_name,item.column_name;
END IF;
END LOOP;
IF EXISTS(SELECT 1 FROM mne.parties WHERE user_id IS NOT NULL) THEN
RAISE EXCEPTION 'Legacy UUID party identities require explicit reconciliation first';
END IF;
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgrelid='mne.audit_events'::regclass AND tgname='mne_evidence_immutable') THEN
RAISE EXCEPTION '003 integrity prerequisite absent';
END IF;
END $preflight$;
-- Retain the unused UUID column for reversibility, but prohibit account bindings through it.
DO $party$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid='mne.parties'::regclass AND conname='ck_parties_identity_links') THEN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conrelid='mne.parties'::regclass
AND conname='parties_check' AND contype='c') THEN
RAISE EXCEPTION 'Unexpected parties shape constraint';
END IF;
ALTER TABLE mne.parties DROP CONSTRAINT parties_check;
ALTER TABLE mne.parties ADD CONSTRAINT ck_parties_identity_links CHECK
(user_id IS NULL AND ((party_type='person' AND organization_id IS NULL)
OR (party_type='organization' AND organization_id IS NOT NULL)));
END IF;
END $party$;
CREATE TABLE IF NOT EXISTS mne.identity_links (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
user_id bigint NOT NULL UNIQUE REFERENCES public.users(id) ON UPDATE RESTRICT ON DELETE RESTRICT,
party_id uuid UNIQUE REFERENCES mne.parties(id) ON UPDATE RESTRICT ON DELETE RESTRICT,
source_system text NOT NULL CHECK(source_system='public.users'),
status text NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','active','revoked')),
valid_from timestamptz NOT NULL DEFAULT now(), valid_until timestamptz,
source_checksum char(64) NOT NULL,
CHECK(valid_until IS NULL OR valid_until>valid_from), CHECK(status<>'active' OR party_id IS NOT NULL)
);
-- This is an identifier mapping, not a second sector taxonomy.
CREATE TABLE IF NOT EXISTS mne.sector_links (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
sector_id bigint NOT NULL UNIQUE REFERENCES public.sectors(id) ON UPDATE RESTRICT ON DELETE RESTRICT,
canonical_sector_id uuid NOT NULL UNIQUE,
source_system text NOT NULL CHECK(source_system='public.sectors'), source_code text NOT NULL UNIQUE,
resolver_version integer NOT NULL DEFAULT 1 CHECK(resolver_version=1),
status text NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','active','revoked')),
valid_from timestamptz NOT NULL DEFAULT now(), valid_until timestamptz, source_checksum char(64) NOT NULL,
CHECK(valid_until IS NULL OR valid_until>valid_from)
);
CREATE TABLE IF NOT EXISTS mne.business_links (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
legacy_business_id uuid NOT NULL UNIQUE REFERENCES public.existing_businesses(id) ON UPDATE RESTRICT ON DELETE RESTRICT,
canonical_business_id uuid UNIQUE REFERENCES mne.existing_businesses(id) ON UPDATE RESTRICT ON DELETE RESTRICT,
source_system text NOT NULL CHECK(source_system='public.existing_businesses'),
status text NOT NULL DEFAULT 'pending' CHECK(status IN ('pending','active','revoked')),
write_authority text NOT NULL DEFAULT 'legacy' CHECK(write_authority='legacy'),
valid_from timestamptz NOT NULL DEFAULT now(), valid_until timestamptz,
source_checksum char(64) NOT NULL, reconciled_version integer,
CHECK(valid_until IS NULL OR valid_until>valid_from),
CHECK(status<>'active' OR (canonical_business_id IS NOT NULL AND reconciled_version IS NOT NULL))
);
CREATE TABLE IF NOT EXISTS mne.compatibility_checkpoints (
domain text PRIMARY KEY CHECK(domain IN ('identity','sector','business')),
last_key text, processed bigint NOT NULL DEFAULT 0, checksum char(64), updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS mne.compatibility_rejections (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(), domain text NOT NULL, source_key text NOT NULL,
code text NOT NULL, cause text NOT NULL, responsible text NOT NULL DEFAULT 'data-steward',
resolution text, resolved_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(),
UNIQUE(domain,source_key,code), CHECK((resolved_at IS NULL)=(resolution IS NULL))
);
CREATE TABLE IF NOT EXISTS mne.compatibility_controls (
capability text PRIMARY KEY CHECK(capability IN ('shadow','canonical_read')),
enabled boolean NOT NULL DEFAULT false,
percentage integer NOT NULL DEFAULT 0 CHECK(percentage BETWEEN 0 AND 100),
-- Activation also requires a compatible adapter and per-record reconciliation.
evidence_reference text, updated_at timestamptz NOT NULL DEFAULT now(),
CHECK(NOT enabled OR length(btrim(evidence_reference))>=10)
);
INSERT INTO mne.compatibility_controls(capability) VALUES ('shadow'),('canonical_read') ON CONFLICT DO NOTHING;
DO $record$
DECLARE name text; relation regclass; signature text;
BEGIN
FOREACH name IN ARRAY ARRAY['identity_links','sector_links','business_links','compatibility_checkpoints','compatibility_rejections','compatibility_controls'] LOOP
relation := to_regclass('mne.'||name);
SELECT md5(COALESCE((SELECT string_agg(a.attname||':'||format_type(a.atttypid,a.atttypmod)||':'||a.attnotnull::text||':'||COALESCE(pg_get_expr(d.adbin,d.adrelid),''),'|' ORDER BY a.attnum)
FROM pg_attribute a LEFT JOIN pg_attrdef d ON d.adrelid=a.attrelid AND d.adnum=a.attnum WHERE a.attrelid=relation AND a.attnum>0 AND NOT a.attisdropped),'')||COALESCE((SELECT string_agg(conname||':'||pg_get_constraintdef(oid),'|' ORDER BY conname) FROM pg_constraint WHERE conrelid=relation),'')) INTO signature;
EXECUTE format('COMMENT ON TABLE mne.%I IS %L',name,'004_mne_compatibility:'||signature);
END LOOP;
END $record$;
COMMIT;