119 lines
7.3 KiB
PL/PgSQL
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;
|