330 lines
12 KiB
PL/PgSQL
330 lines
12 KiB
PL/PgSQL
-- ARR MCP direct structured-result ingestion ledger.
|
|
-- PostgreSQL 15+. Additive only; migrations 008 and 009 remain immutable.
|
|
|
|
BEGIN;
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF current_database() <> 'booking_test' THEN
|
|
RAISE EXCEPTION
|
|
'ARR MCP result ingestion is allowed only in booking_test';
|
|
END IF;
|
|
IF to_regclass('ingestion.processing_runs') IS NULL
|
|
OR to_regclass('ingestion.processing_attempts') IS NULL
|
|
OR to_regclass('finance.daily_versions') IS NULL THEN
|
|
RAISE EXCEPTION
|
|
'ARR MVP v1 migration 008 must be applied first';
|
|
END IF;
|
|
IF to_regclass('ingestion.result_submission_grants') IS NOT NULL
|
|
OR to_regclass('ingestion.result_submissions') IS NOT NULL THEN
|
|
RAISE EXCEPTION
|
|
'ARR MCP result ingestion migration 010 is already applied';
|
|
END IF;
|
|
END;
|
|
$$;
|
|
|
|
-- Existing rows retain the artifact callback contract. New MCP runs opt in
|
|
-- explicitly and may finish without Agent-produced result objects in OSS.
|
|
ALTER TABLE ingestion.processing_runs
|
|
ADD COLUMN result_delivery_mode text NOT NULL
|
|
DEFAULT 'artifact_callback';
|
|
|
|
ALTER TABLE ingestion.processing_runs
|
|
ADD CONSTRAINT processing_runs_result_delivery_mode_check CHECK (
|
|
result_delivery_mode IN ('artifact_callback', 'direct_mcp')
|
|
),
|
|
ADD CONSTRAINT processing_runs_result_delivery_pipeline_check CHECK (
|
|
pipeline_type = 'opera_daily'
|
|
OR result_delivery_mode = 'artifact_callback'
|
|
);
|
|
|
|
ALTER TABLE ingestion.processing_runs
|
|
DROP CONSTRAINT processing_runs_terminal_shape;
|
|
|
|
ALTER TABLE ingestion.processing_runs
|
|
ADD CONSTRAINT processing_runs_terminal_shape CHECK (
|
|
(
|
|
run_status = 'accepted'
|
|
AND failure_code IS NULL
|
|
AND validated_at IS NOT NULL
|
|
AND finished_at IS NOT NULL
|
|
AND (
|
|
pipeline_type <> 'opera_daily'
|
|
OR (
|
|
business_date IS NOT NULL
|
|
AND delivered_processor_version IS NOT NULL
|
|
AND delivered_rule_set_sha256 IS NOT NULL
|
|
AND result_schema_version IS NOT NULL
|
|
AND delivery_sha256 IS NOT NULL
|
|
AND (
|
|
(
|
|
result_delivery_mode = 'artifact_callback'
|
|
AND result_artifact_id IS NOT NULL
|
|
)
|
|
OR (
|
|
result_delivery_mode = 'direct_mcp'
|
|
AND result_artifact_id IS NULL
|
|
)
|
|
)
|
|
)
|
|
)
|
|
)
|
|
OR (
|
|
run_status IN ('rejected', 'failed')
|
|
AND failure_code IS NOT NULL
|
|
AND finished_at IS NOT NULL
|
|
)
|
|
OR (
|
|
run_status = 'cancelled'
|
|
AND finished_at IS NOT NULL
|
|
)
|
|
OR run_status IN ('received', 'queued', 'running', 'validating')
|
|
);
|
|
|
|
ALTER TABLE finance.daily_versions
|
|
ADD COLUMN result_delivery_mode text NOT NULL
|
|
DEFAULT 'artifact_callback';
|
|
|
|
ALTER TABLE finance.daily_versions
|
|
ADD CONSTRAINT daily_versions_result_delivery_mode_check CHECK (
|
|
result_delivery_mode IN ('artifact_callback', 'direct_mcp')
|
|
),
|
|
ADD CONSTRAINT daily_versions_result_delivery_shape CHECK (
|
|
result_delivery_mode <> 'direct_mcp'
|
|
OR (
|
|
daily_report_artifact_id IS NULL
|
|
AND result_json_artifact_id IS NULL
|
|
AND structured_result_artifact_id IS NULL
|
|
AND exception_report_artifact_id IS NULL
|
|
)
|
|
);
|
|
|
|
ALTER TABLE finance.daily_versions
|
|
DROP CONSTRAINT daily_versions_status_shape;
|
|
|
|
ALTER TABLE finance.daily_versions
|
|
ADD CONSTRAINT daily_versions_status_shape CHECK (
|
|
(
|
|
version_status IN ('validated', 'active', 'superseded')
|
|
AND business_date IS NOT NULL
|
|
AND exception_report_artifact_id IS NULL
|
|
AND failure_code IS NULL
|
|
AND validated_at IS NOT NULL
|
|
AND (
|
|
(
|
|
result_delivery_mode = 'artifact_callback'
|
|
AND daily_report_artifact_id IS NOT NULL
|
|
AND result_json_artifact_id IS NOT NULL
|
|
AND structured_result_artifact_id IS NOT NULL
|
|
)
|
|
OR (
|
|
result_delivery_mode = 'direct_mcp'
|
|
AND daily_report_artifact_id IS NULL
|
|
AND result_json_artifact_id IS NULL
|
|
AND structured_result_artifact_id IS NULL
|
|
)
|
|
)
|
|
)
|
|
OR (
|
|
version_status = 'rejected'
|
|
AND failure_code IS NOT NULL
|
|
)
|
|
);
|
|
|
|
CREATE TABLE ingestion.result_submission_grants (
|
|
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
grant_sha256 character(64) NOT NULL UNIQUE CHECK (
|
|
grant_sha256 ~ '^[0-9a-f]{64}$'
|
|
),
|
|
processing_run_id bigint NOT NULL
|
|
REFERENCES ingestion.processing_runs(id),
|
|
attempt_id bigint NOT NULL,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
expires_at timestamptz NOT NULL,
|
|
consumed_at timestamptz,
|
|
revoked_at timestamptz,
|
|
CONSTRAINT result_submission_grants_attempt_unique
|
|
UNIQUE (attempt_id),
|
|
CONSTRAINT result_submission_grants_id_run_attempt_unique
|
|
UNIQUE (id, processing_run_id, attempt_id),
|
|
CONSTRAINT result_submission_grants_attempt_run_fk
|
|
FOREIGN KEY (attempt_id, processing_run_id)
|
|
REFERENCES ingestion.processing_attempts(id, processing_run_id),
|
|
CONSTRAINT result_submission_grants_lifetime CHECK (
|
|
expires_at > created_at
|
|
AND expires_at <= created_at + interval '30 minutes'
|
|
),
|
|
CONSTRAINT result_submission_grants_usage_shape CHECK (
|
|
NOT (consumed_at IS NOT NULL AND revoked_at IS NOT NULL)
|
|
AND (
|
|
consumed_at IS NULL
|
|
OR consumed_at BETWEEN created_at AND expires_at
|
|
)
|
|
AND (revoked_at IS NULL OR revoked_at >= created_at)
|
|
)
|
|
);
|
|
|
|
CREATE INDEX result_submission_grants_expiry_idx
|
|
ON ingestion.result_submission_grants (expires_at)
|
|
WHERE consumed_at IS NULL AND revoked_at IS NULL;
|
|
|
|
CREATE TABLE ingestion.result_submissions (
|
|
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
submission_key text NOT NULL UNIQUE CHECK (
|
|
submission_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$'
|
|
),
|
|
grant_id bigint NOT NULL UNIQUE,
|
|
processing_run_id bigint NOT NULL,
|
|
attempt_id bigint NOT NULL UNIQUE,
|
|
submission_status text NOT NULL CHECK (submission_status IN (
|
|
'received',
|
|
'validating',
|
|
'committed',
|
|
'already_committed',
|
|
'rejected',
|
|
'expired'
|
|
)),
|
|
contract_version text NOT NULL CHECK (
|
|
contract_version = 'arr-direct-ingestion-1'
|
|
),
|
|
business_date date NOT NULL,
|
|
processor_version text NOT NULL CHECK (
|
|
processor_version ~ '^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$'
|
|
),
|
|
rule_set_sha256 character(64) NOT NULL CHECK (
|
|
rule_set_sha256 ~ '^[0-9a-f]{64}$'
|
|
),
|
|
result_schema_version text NOT NULL CHECK (
|
|
btrim(result_schema_version) <> ''
|
|
),
|
|
payload_json jsonb CHECK (
|
|
payload_json IS NULL
|
|
OR jsonb_typeof(payload_json) = 'object'
|
|
),
|
|
payload_sha256 character(64) NOT NULL CHECK (
|
|
payload_sha256 ~ '^[0-9a-f]{64}$'
|
|
),
|
|
payload_byte_size integer NOT NULL CHECK (
|
|
payload_byte_size BETWEEN 2 AND 3145728
|
|
),
|
|
record_count integer NOT NULL CHECK (
|
|
record_count >= 0
|
|
),
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
expires_at timestamptz NOT NULL,
|
|
validation_started_at timestamptz,
|
|
finished_at timestamptz,
|
|
payload_purged_at timestamptz,
|
|
daily_version_id bigint
|
|
REFERENCES finance.daily_versions(id),
|
|
failure_code text,
|
|
failure_message text,
|
|
receipt_json jsonb NOT NULL DEFAULT '{}'::jsonb CHECK (
|
|
jsonb_typeof(receipt_json) = 'object'
|
|
),
|
|
CONSTRAINT result_submissions_grant_run_attempt_fk
|
|
FOREIGN KEY (grant_id, processing_run_id, attempt_id)
|
|
REFERENCES ingestion.result_submission_grants(
|
|
id,
|
|
processing_run_id,
|
|
attempt_id
|
|
),
|
|
CONSTRAINT result_submissions_lifetime CHECK (
|
|
expires_at > created_at
|
|
AND expires_at <= created_at + interval '30 minutes'
|
|
),
|
|
CONSTRAINT result_submissions_payload_shape CHECK (
|
|
payload_json IS NULL
|
|
OR (
|
|
payload_json ? 'records'
|
|
AND jsonb_typeof(payload_json -> 'records') = 'array'
|
|
AND jsonb_array_length(payload_json -> 'records') = record_count
|
|
)
|
|
),
|
|
CONSTRAINT result_submissions_timestamps_valid CHECK (
|
|
(
|
|
validation_started_at IS NULL
|
|
OR validation_started_at >= created_at
|
|
)
|
|
AND (finished_at IS NULL OR finished_at >= created_at)
|
|
AND (
|
|
payload_purged_at IS NULL
|
|
OR (
|
|
finished_at IS NOT NULL
|
|
AND payload_purged_at >= finished_at
|
|
)
|
|
)
|
|
AND (
|
|
submission_status <> 'expired'
|
|
OR finished_at >= expires_at
|
|
)
|
|
),
|
|
CONSTRAINT result_submissions_status_shape CHECK (
|
|
(
|
|
submission_status = 'received'
|
|
AND validation_started_at IS NULL
|
|
AND finished_at IS NULL
|
|
AND daily_version_id IS NULL
|
|
AND failure_code IS NULL
|
|
AND receipt_json = '{}'::jsonb
|
|
AND payload_json IS NOT NULL
|
|
AND payload_purged_at IS NULL
|
|
)
|
|
OR (
|
|
submission_status = 'validating'
|
|
AND validation_started_at IS NOT NULL
|
|
AND finished_at IS NULL
|
|
AND daily_version_id IS NULL
|
|
AND failure_code IS NULL
|
|
AND receipt_json = '{}'::jsonb
|
|
AND payload_json IS NOT NULL
|
|
AND payload_purged_at IS NULL
|
|
)
|
|
OR (
|
|
submission_status IN ('committed', 'already_committed')
|
|
AND validation_started_at IS NOT NULL
|
|
AND finished_at IS NOT NULL
|
|
AND daily_version_id IS NOT NULL
|
|
AND failure_code IS NULL
|
|
AND receipt_json <> '{}'::jsonb
|
|
AND payload_json IS NULL
|
|
AND payload_purged_at IS NOT NULL
|
|
)
|
|
OR (
|
|
submission_status IN ('rejected', 'expired')
|
|
AND finished_at IS NOT NULL
|
|
AND daily_version_id IS NULL
|
|
AND failure_code IS NOT NULL
|
|
AND btrim(failure_code) <> ''
|
|
AND payload_json IS NULL
|
|
AND payload_purged_at IS NOT NULL
|
|
)
|
|
)
|
|
);
|
|
|
|
CREATE INDEX result_submissions_status_expiry_idx
|
|
ON ingestion.result_submissions (submission_status, expires_at);
|
|
|
|
-- Deliberately non-unique: the same XML/result may be processed repeatedly by
|
|
-- different test jobs while final Finance facts remain idempotent.
|
|
CREATE INDEX result_submissions_payload_sha256_idx
|
|
ON ingestion.result_submissions (payload_sha256);
|
|
|
|
CREATE INDEX result_submissions_daily_version_idx
|
|
ON ingestion.result_submissions (daily_version_id)
|
|
WHERE daily_version_id IS NOT NULL;
|
|
|
|
COMMENT ON COLUMN ingestion.processing_runs.result_delivery_mode IS
|
|
'artifact_callback preserves the v3 OSS result contract; direct_mcp commits validated structured records without Agent-produced output objects.';
|
|
|
|
COMMENT ON COLUMN finance.daily_versions.result_delivery_mode IS
|
|
'Identifies whether the validated Finance version arrived through the legacy artifact callback or the direct MCP ingestion path.';
|
|
|
|
COMMENT ON TABLE ingestion.result_submission_grants IS
|
|
'Hashed, attempt-bound grants for starting one direct MCP result submission. Plaintext grants and database credentials are never stored.';
|
|
|
|
COMMENT ON TABLE ingestion.result_submissions IS
|
|
'Durable, idempotent single-call result ledger. Payload hashes are intentionally non-unique so the same XML can be rerun under a new processing attempt; guest-bearing payload JSON is purged at terminal status.';
|
|
|
|
COMMIT;
|