-- Project CT: permanent, versioned and application-read-only vTiger archive. CREATE TABLE IF NOT EXISTS vtiger_archive_versions ( id BIGSERIAL PRIMARY KEY, sync_kind VARCHAR(20) NOT NULL CHECK (sync_kind IN ('full','incremental','final')), status VARCHAR(20) NOT NULL DEFAULT 'running' CHECK (status IN ('running','completed','failed')), started_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), completed_at TIMESTAMPTZ, source_cutoff TIMESTAMPTZ NOT NULL DEFAULT NOW(), initiated_by INTEGER REFERENCES users(user_id) ON DELETE SET NULL, module_counts JSONB NOT NULL DEFAULT '{}'::jsonb, warnings JSONB NOT NULL DEFAULT '[]'::jsonb, critical_errors JSONB NOT NULL DEFAULT '[]'::jsonb, control_report JSONB NOT NULL DEFAULT '{}'::jsonb, control_approved_at TIMESTAMPTZ, control_approved_by INTEGER REFERENCES users(user_id) ON DELETE SET NULL, raw_export_sha256 VARCHAR(64), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS vtiger_archive_records ( id BIGSERIAL PRIMARY KEY, version_id BIGINT NOT NULL REFERENCES vtiger_archive_versions(id) ON DELETE RESTRICT, module VARCHAR(80) NOT NULL, vtiger_id VARCHAR(120) NOT NULL, revision_no INTEGER NOT NULL, source_created_at TIMESTAMPTZ, source_modified_at TIMESTAMPTZ, is_deleted BOOLEAN NOT NULL DEFAULT FALSE, payload JSONB NOT NULL, payload_sha256 VARCHAR(64) NOT NULL, archived_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(module, vtiger_id, revision_no), UNIQUE(version_id, module, vtiger_id, payload_sha256) ); CREATE INDEX IF NOT EXISTS idx_vtiger_archive_records_lookup ON vtiger_archive_records(module, vtiger_id, revision_no DESC); CREATE INDEX IF NOT EXISTS idx_vtiger_archive_records_version ON vtiger_archive_records(version_id, module); CREATE TABLE IF NOT EXISTS vtiger_archive_relations ( id BIGSERIAL PRIMARY KEY, version_id BIGINT NOT NULL REFERENCES vtiger_archive_versions(id) ON DELETE RESTRICT, source_module VARCHAR(80) NOT NULL, source_vtiger_id VARCHAR(120) NOT NULL, field_name VARCHAR(120) NOT NULL, target_vtiger_id VARCHAR(120) NOT NULL, target_module VARCHAR(80), UNIQUE(version_id, source_module, source_vtiger_id, field_name, target_vtiger_id) ); CREATE INDEX IF NOT EXISTS idx_vtiger_archive_relations_target ON vtiger_archive_relations(target_vtiger_id); CREATE TABLE IF NOT EXISTS vtiger_archive_checkpoints ( module VARCHAR(80) PRIMARY KEY, last_modified_at TIMESTAMPTZ, last_vtiger_id VARCHAR(120), last_successful_version_id BIGINT REFERENCES vtiger_archive_versions(id) ON DELETE SET NULL, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS hub_impact_reports ( id BIGSERIAL PRIMARY KEY, archive_version_id BIGINT NOT NULL REFERENCES vtiger_archive_versions(id) ON DELETE RESTRICT, vtiger_from DATE NOT NULL, vtiger_to DATE NOT NULL, hub_from DATE NOT NULL, hub_to DATE NOT NULL, filters JSONB NOT NULL DEFAULT '{}'::jsonb, result JSONB NOT NULL, result_sha256 VARCHAR(64) NOT NULL, generated_by INTEGER REFERENCES users(user_id) ON DELETE SET NULL, generated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE OR REPLACE FUNCTION deny_vtiger_archive_mutation() RETURNS trigger AS $$ BEGIN RAISE EXCEPTION 'vTiger archive revisions are append-only'; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS trg_vtiger_archive_records_immutable ON vtiger_archive_records; CREATE TRIGGER trg_vtiger_archive_records_immutable BEFORE UPDATE OR DELETE ON vtiger_archive_records FOR EACH ROW EXECUTE FUNCTION deny_vtiger_archive_mutation();