Files
ARR-2.0-0918/database/022_daily_report_retirement.sql
T

171 lines
7.2 KiB
PL/PgSQL

-- Recoverable daily retirement and withdrawal of stale monthly publications.
-- PostgreSQL 15+. Migrations 008 through 021 remain immutable.
BEGIN;
DO $$
BEGIN
IF current_database() <> 'booking_test' THEN
RAISE EXCEPTION 'ARR daily lifecycle migration is allowed only in booking_test';
END IF;
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'finance' AND table_name = 'daily_versions'
AND column_name = 'excluded_pm_rows'
) OR to_regprocedure('reporting.protect_published_monthly_child()') IS NULL THEN
RAISE EXCEPTION 'ARR migrations 008 through 021 must be applied before 022';
END IF;
IF to_regclass('ingestion.daily_run_retirements') IS NOT NULL THEN
RAISE EXCEPTION 'ARR daily lifecycle migration 022 is already applied';
END IF;
END $$;
CREATE TABLE ingestion.daily_run_retirements (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
processing_run_id bigint NOT NULL UNIQUE REFERENCES ingestion.processing_runs(id),
business_date date NOT NULL,
daily_version_id bigint REFERENCES finance.daily_versions(id),
current_removed boolean NOT NULL,
actor_username text NOT NULL CHECK (btrim(actor_username) <> '' AND char_length(actor_username) <= 255),
retired_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT daily_run_retirements_current_shape CHECK (NOT current_removed OR daily_version_id IS NOT NULL)
);
COMMENT ON TABLE ingestion.daily_run_retirements IS
'Append-only retirement receipts. Sources, processing runs, original Finance facts and prior publications are retained; no older daily version is automatically restored.';
CREATE FUNCTION ingestion.protect_daily_run_retirement()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF TG_OP <> 'INSERT' THEN
RAISE EXCEPTION 'daily retirement audit is immutable';
END IF;
IF NOT EXISTS (
SELECT 1 FROM ingestion.processing_runs AS run
WHERE run.id = NEW.processing_run_id AND run.pipeline_type = 'opera_daily'
AND run.business_date = NEW.business_date
AND run.run_status IN ('accepted', 'rejected', 'failed', 'cancelled')
) OR (NEW.daily_version_id IS NOT NULL AND NOT EXISTS (
SELECT 1 FROM finance.daily_versions AS version
WHERE version.id = NEW.daily_version_id AND version.processing_run_id = NEW.processing_run_id
AND (version.business_date = NEW.business_date OR version.version_status = 'rejected')
)) THEN
RAISE EXCEPTION 'daily retirement must reference its terminal run and exact daily version';
END IF;
IF EXISTS (
SELECT 1 FROM finance.current_daily_versions AS current_version
WHERE current_version.daily_version_id = NEW.daily_version_id
) THEN
RAISE EXCEPTION 'a retired daily version cannot remain the current daily source';
END IF;
RETURN NEW;
END $$;
CREATE TRIGGER daily_run_retirements_immutable
BEFORE INSERT OR UPDATE OR DELETE ON ingestion.daily_run_retirements
FOR EACH ROW EXECUTE FUNCTION ingestion.protect_daily_run_retirement();
CREATE OR REPLACE FUNCTION finance.validate_current_daily_version()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM finance.daily_versions AS version
WHERE version.id = NEW.daily_version_id
AND version.business_date = NEW.business_date
AND version.version_status = 'active'
AND NOT EXISTS (
SELECT 1 FROM ingestion.daily_run_retirements AS retired
WHERE retired.processing_run_id = version.processing_run_id
)
) THEN
RAISE EXCEPTION 'current business date must reference an active non-retired daily version';
END IF;
RETURN NEW;
END $$;
ALTER TABLE reporting.monthly_runs
ADD COLUMN withdrawn_at timestamptz,
DROP CONSTRAINT monthly_runs_report_status_check,
DROP CONSTRAINT monthly_runs_status_shape,
DROP CONSTRAINT monthly_runs_snapshot_unique;
ALTER TABLE reporting.monthly_runs
ADD CONSTRAINT monthly_runs_report_status_check CHECK (report_status IN (
'reserved', 'active', 'superseded', 'failed', 'withdrawn'
)),
ADD CONSTRAINT monthly_runs_status_shape CHECK (
(withdrawn_at IS NULL AND (
(
report_status = 'reserved'
AND workbook_artifact_id IS NULL
AND result_artifact_id IS NULL
AND semantic_sha256 IS NULL
AND failure_code IS NULL
AND failure_message IS NULL
AND published_at IS NULL
AND superseded_at IS NULL
AND failed_at IS NULL
)
OR (
report_status = 'active'
AND workbook_artifact_id IS NOT NULL
AND result_artifact_id IS NOT NULL
AND semantic_sha256 IS NOT NULL
AND failure_code IS NULL
AND failure_message IS NULL
AND published_at IS NOT NULL
AND superseded_at IS NULL
AND failed_at IS NULL
)
OR (
report_status = 'superseded'
AND workbook_artifact_id IS NOT NULL
AND result_artifact_id IS NOT NULL
AND semantic_sha256 IS NOT NULL
AND failure_code IS NULL
AND failure_message IS NULL
AND published_at IS NOT NULL
AND superseded_at IS NOT NULL
AND failed_at IS NULL
)
OR (
report_status = 'failed'
AND workbook_artifact_id IS NULL
AND result_artifact_id IS NULL
AND semantic_sha256 IS NULL
AND failure_code IS NOT NULL
AND btrim(failure_code) <> ''
AND published_at IS NULL
AND superseded_at IS NULL
AND failed_at IS NOT NULL
)
)) OR (report_status = 'withdrawn' AND withdrawn_at IS NOT NULL)
);
CREATE UNIQUE INDEX monthly_runs_live_snapshot_unique
ON reporting.monthly_runs (period_start, source_snapshot_sha256)
WHERE report_status <> 'withdrawn';
CREATE OR REPLACE FUNCTION reporting.protect_published_monthly_child()
RETURNS trigger LANGUAGE plpgsql AS $$
DECLARE parent_id bigint;
BEGIN
parent_id := COALESCE(OLD.report_id, NEW.report_id);
IF EXISTS (
SELECT 1 FROM reporting.monthly_runs AS run
WHERE run.id = parent_id AND run.report_status IN ('active', 'superseded', 'withdrawn')
) THEN
RAISE EXCEPTION 'published monthly report lineage and manifest are immutable';
END IF;
RETURN COALESCE(NEW, OLD);
END $$;
COMMENT ON COLUMN reporting.monthly_runs.withdrawn_at IS
'Publication removed from downloads because a current daily source was retired. Historical artifacts and lineage remain intact; reserved builds cannot later publish.';
-- Existing consuming application role; no source, artifact or Finance-fact
-- DELETE privileges are added. Differently named roles require equivalent
-- explicit grants in the release deployment, never PUBLIC or web read roles.
DO $$
BEGIN
IF to_regrole('arr_app') IS NOT NULL THEN
EXECUTE 'GRANT SELECT, INSERT ON ingestion.daily_run_retirements TO arr_app';
EXECUTE 'GRANT USAGE, SELECT ON SEQUENCE ingestion.daily_run_retirements_id_seq TO arr_app';
EXECUTE 'GRANT DELETE ON finance.current_daily_versions TO arr_app';
END IF;
END $$;
COMMIT;