Manage daily reports by date and refresh monthly reports after deletion
This commit is contained in:
1 parent
976a4fa7b2
commit
2a54838d59
41 files changed
+2757
-89
No files matched your search
@@ -0,0 +1,111 @@
|
||||
-- Operator-only rollback; refuses to discard any retirement or withdrawn publication.
|
||||
BEGIN;
|
||||
DO $$
|
||||
BEGIN
|
||||
IF current_database() <> 'booking_test' THEN
|
||||
RAISE EXCEPTION 'ARR daily lifecycle rollback is allowed only in booking_test';
|
||||
END IF;
|
||||
IF EXISTS (SELECT 1 FROM ingestion.daily_run_retirements)
|
||||
OR EXISTS (SELECT 1 FROM reporting.monthly_runs WHERE report_status = 'withdrawn' OR withdrawn_at IS NOT NULL) THEN
|
||||
RAISE EXCEPTION 'refusing rollback: daily retirement or withdrawn monthly audit exists';
|
||||
END IF;
|
||||
END $$;
|
||||
DROP TRIGGER daily_run_retirements_immutable ON ingestion.daily_run_retirements;
|
||||
DROP FUNCTION ingestion.protect_daily_run_retirement();
|
||||
DROP TABLE ingestion.daily_run_retirements;
|
||||
DROP INDEX reporting.monthly_runs_live_snapshot_unique;
|
||||
ALTER TABLE reporting.monthly_runs
|
||||
DROP CONSTRAINT monthly_runs_report_status_check,
|
||||
DROP CONSTRAINT monthly_runs_status_shape,
|
||||
DROP COLUMN withdrawn_at;
|
||||
ALTER TABLE reporting.monthly_runs
|
||||
ADD CONSTRAINT monthly_runs_report_status_check CHECK (report_status IN ('reserved', 'active', 'superseded', 'failed')),
|
||||
ADD CONSTRAINT monthly_runs_snapshot_unique UNIQUE (period_start, source_snapshot_sha256),
|
||||
ADD CONSTRAINT monthly_runs_status_shape CHECK (
|
||||
(
|
||||
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
|
||||
)
|
||||
);
|
||||
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'
|
||||
) THEN
|
||||
RAISE EXCEPTION
|
||||
'current business date must reference an active daily version';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
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')
|
||||
) THEN
|
||||
RAISE EXCEPTION
|
||||
'published monthly report lineage and manifest are immutable';
|
||||
END IF;
|
||||
RETURN COALESCE(NEW, OLD);
|
||||
END;
|
||||
$$;
|
||||
|
||||
|
||||
COMMIT;
|
||||
@@ -0,0 +1,170 @@
|
||||
-- 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;
|
||||
Reference in new issue
Block a user