Files
wyndham-ARR/database/010_mcp_result_ingestion.sql
2026-07-29 16:38:05 +08:00

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;