Import the committed KnowledgeFS snapshot dc4072ee302317145612087ce7440851dc329fd0 under knowledge-fs/ without its Git history, local IDE settings, or build artifacts.
656 lines
26 KiB
SQL
656 lines
26 KiB
SQL
-- Knowledge Platform schema migration
|
|
-- Migration id: 0017_durable_deletion
|
|
-- Dialect: postgres
|
|
|
|
-- Resource rows remain present while deletion is in progress. Defaults preserve rolling-upgrade
|
|
-- compatibility for old writers; the application only enables the deletion endpoint after all
|
|
-- writers understand these fences.
|
|
ALTER TABLE "knowledge_spaces"
|
|
ADD COLUMN IF NOT EXISTS "revision" INTEGER NOT NULL DEFAULT 1,
|
|
ADD COLUMN IF NOT EXISTS "lifecycle_state" VARCHAR(16) NOT NULL DEFAULT 'active',
|
|
ADD COLUMN IF NOT EXISTS "deletion_job_id" UUID,
|
|
ADD COLUMN IF NOT EXISTS "deleting_at" TIMESTAMPTZ;
|
|
|
|
ALTER TABLE "sources"
|
|
ADD COLUMN IF NOT EXISTS "deletion_job_id" UUID,
|
|
ADD COLUMN IF NOT EXISTS "deleting_at" TIMESTAMPTZ;
|
|
|
|
ALTER TABLE "document_assets"
|
|
ADD COLUMN IF NOT EXISTS "lifecycle_state" VARCHAR(16) NOT NULL DEFAULT 'active',
|
|
ADD COLUMN IF NOT EXISTS "deletion_job_id" UUID,
|
|
ADD COLUMN IF NOT EXISTS "deleting_at" TIMESTAMPTZ,
|
|
ADD COLUMN IF NOT EXISTS "row_version" INTEGER NOT NULL DEFAULT 1;
|
|
|
|
ALTER TABLE "knowledge_space_mutation_leases"
|
|
ADD COLUMN IF NOT EXISTS "lease_token" UUID,
|
|
ADD COLUMN IF NOT EXISTS "heartbeat_at" TIMESTAMPTZ,
|
|
ADD COLUMN IF NOT EXISTS "expires_at" TIMESTAMPTZ;
|
|
-- Rows created by the pre-0017 ownerless lease protocol are made immediately reclaimable. A live
|
|
-- writer remains fenced by the resource tombstone even if it returns after this compatibility cut.
|
|
UPDATE "knowledge_space_mutation_leases"
|
|
SET "lease_token" = "id",
|
|
"heartbeat_at" = "acquired_at",
|
|
"expires_at" = "acquired_at"
|
|
WHERE "lease_token" IS NULL OR "heartbeat_at" IS NULL OR "expires_at" IS NULL;
|
|
|
|
-- The catalog models manifests as tenant-owned rows. The original schema constrained only the
|
|
-- UUID, so fail closed on historical cross-tenant rows before installing the composite invariant.
|
|
-- Add the stronger FK before removing the legacy one so marker-loss replay never leaves a gap.
|
|
DO $kfs_0017_knowledge_space_manifests_space_fk$
|
|
BEGIN
|
|
IF EXISTS (
|
|
SELECT 1
|
|
FROM "knowledge_space_manifests" AS manifest
|
|
LEFT JOIN "knowledge_spaces" AS space
|
|
ON space."tenant_id" = manifest."tenant_id"
|
|
AND space."id" = manifest."knowledge_space_id"
|
|
WHERE space."id" IS NULL
|
|
) THEN
|
|
RAISE EXCEPTION
|
|
'knowledge_space_manifests contains a tenant/space ownership mismatch';
|
|
END IF;
|
|
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM "pg_constraint"
|
|
WHERE "conrelid" = 'knowledge_space_manifests'::regclass
|
|
AND "conname" = 'knowledge_space_manifests_space_fk'
|
|
) THEN
|
|
ALTER TABLE "knowledge_space_manifests"
|
|
ADD CONSTRAINT "knowledge_space_manifests_space_fk"
|
|
FOREIGN KEY ("tenant_id", "knowledge_space_id")
|
|
REFERENCES "knowledge_spaces" ("tenant_id", "id") ON DELETE CASCADE;
|
|
END IF;
|
|
|
|
ALTER TABLE "knowledge_space_manifests"
|
|
DROP CONSTRAINT IF EXISTS "knowledge_space_manifests_knowledge_space_id_fkey";
|
|
END
|
|
$kfs_0017_knowledge_space_manifests_space_fk$;
|
|
|
|
-- Evidence bundles were originally ownerless. Keep the columns nullable for a rolling writer
|
|
-- upgrade, backfill only when every durable reference agrees on one tenant/space, and quarantine
|
|
-- ambiguous rows by leaving them NULL. Application reads fail closed and rollout readiness
|
|
-- requires the NULL population to be purged before durable deletion is enabled.
|
|
ALTER TABLE "evidence_bundles"
|
|
ADD COLUMN IF NOT EXISTS "tenant_id" VARCHAR(255),
|
|
ADD COLUMN IF NOT EXISTS "knowledge_space_id" UUID;
|
|
|
|
WITH evidence_scope_candidates AS (
|
|
SELECT evidence."id" AS bundle_id,
|
|
space."tenant_id" AS tenant_id,
|
|
trace."knowledge_space_id" AS knowledge_space_id
|
|
FROM "evidence_bundles" AS evidence
|
|
INNER JOIN "answer_traces" AS trace
|
|
ON trace."evidence_bundle_id" = evidence."id" OR trace."id" = evidence."trace_id"
|
|
INNER JOIN "knowledge_spaces" AS space
|
|
ON space."id" = trace."knowledge_space_id"
|
|
UNION ALL
|
|
SELECT evidence."id" AS bundle_id,
|
|
partial."tenant_id" AS tenant_id,
|
|
partial."knowledge_space_id" AS knowledge_space_id
|
|
FROM "evidence_bundles" AS evidence
|
|
INNER JOIN "research_task_partial_results" AS partial
|
|
ON CASE
|
|
WHEN jsonb_typeof(partial."evidence_bundle") = 'object'
|
|
THEN partial."evidence_bundle" ->> 'id'
|
|
ELSE NULL
|
|
END = CAST(evidence."id" AS TEXT)
|
|
INNER JOIN "knowledge_spaces" AS space
|
|
ON space."tenant_id" = partial."tenant_id"
|
|
AND space."id" = partial."knowledge_space_id"
|
|
), distinct_candidate_scopes AS (
|
|
SELECT DISTINCT bundle_id, tenant_id, knowledge_space_id
|
|
FROM evidence_scope_candidates
|
|
), unambiguous_evidence_scopes AS (
|
|
SELECT bundle_id,
|
|
MIN(tenant_id) AS tenant_id,
|
|
MIN(CAST(knowledge_space_id AS TEXT))::UUID AS knowledge_space_id
|
|
FROM distinct_candidate_scopes
|
|
GROUP BY bundle_id
|
|
HAVING COUNT(*) = 1
|
|
)
|
|
UPDATE "evidence_bundles" AS evidence
|
|
SET "tenant_id" = resolved."tenant_id",
|
|
"knowledge_space_id" = resolved."knowledge_space_id"
|
|
FROM unambiguous_evidence_scopes AS resolved
|
|
WHERE evidence."id" = resolved.bundle_id
|
|
AND evidence."tenant_id" IS NULL
|
|
AND evidence."knowledge_space_id" IS NULL;
|
|
|
|
DO $kfs_0017_evidence_bundles_scope_pair_ck$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM "pg_constraint"
|
|
WHERE "conrelid" = 'evidence_bundles'::regclass
|
|
AND "conname" = 'evidence_bundles_scope_pair_ck'
|
|
) THEN
|
|
ALTER TABLE "evidence_bundles"
|
|
ADD CONSTRAINT "evidence_bundles_scope_pair_ck" CHECK (
|
|
("tenant_id" IS NULL AND "knowledge_space_id" IS NULL)
|
|
OR ("tenant_id" IS NOT NULL AND "knowledge_space_id" IS NOT NULL)
|
|
);
|
|
END IF;
|
|
END
|
|
$kfs_0017_evidence_bundles_scope_pair_ck$;
|
|
|
|
DO $kfs_0017_evidence_bundles_scope_fk$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM "pg_constraint"
|
|
WHERE "conrelid" = 'evidence_bundles'::regclass
|
|
AND "conname" = 'evidence_bundles_scope_fk'
|
|
) THEN
|
|
ALTER TABLE "evidence_bundles"
|
|
ADD CONSTRAINT "evidence_bundles_scope_fk"
|
|
FOREIGN KEY ("tenant_id", "knowledge_space_id")
|
|
REFERENCES "knowledge_spaces" ("tenant_id", "id") ON DELETE CASCADE;
|
|
END IF;
|
|
END
|
|
$kfs_0017_evidence_bundles_scope_fk$;
|
|
|
|
CREATE INDEX IF NOT EXISTS "evidence_bundles_scope_created_idx"
|
|
ON "evidence_bundles" ("tenant_id", "knowledge_space_id", "created_at", "id");
|
|
|
|
DO $kfs_0017_knowledge_spaces_deletion_lifecycle_ck$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM "pg_constraint"
|
|
WHERE "conrelid" = 'knowledge_spaces'::regclass
|
|
AND "conname" = 'knowledge_spaces_deletion_lifecycle_ck'
|
|
) THEN
|
|
ALTER TABLE "knowledge_spaces"
|
|
ADD CONSTRAINT "knowledge_spaces_deletion_lifecycle_ck" CHECK (
|
|
"revision" >= 1
|
|
AND (
|
|
("lifecycle_state" = 'active' AND "deletion_job_id" IS NULL AND "deleting_at" IS NULL)
|
|
OR
|
|
("lifecycle_state" = 'deleting' AND "deletion_job_id" IS NOT NULL AND "deleting_at" IS NOT NULL)
|
|
)
|
|
);
|
|
END IF;
|
|
END
|
|
$kfs_0017_knowledge_spaces_deletion_lifecycle_ck$;
|
|
|
|
DO $kfs_0017_sources_deletion_lifecycle_ck$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM "pg_constraint"
|
|
WHERE "conrelid" = 'sources'::regclass
|
|
AND "conname" = 'sources_deletion_lifecycle_ck'
|
|
) THEN
|
|
ALTER TABLE "sources"
|
|
ADD CONSTRAINT "sources_deletion_lifecycle_ck" CHECK (
|
|
(
|
|
"status" = 'deleting'
|
|
AND "deletion_job_id" IS NOT NULL
|
|
AND "deleting_at" IS NOT NULL
|
|
)
|
|
OR (
|
|
"status" <> 'deleting'
|
|
AND "deletion_job_id" IS NULL
|
|
AND "deleting_at" IS NULL
|
|
)
|
|
);
|
|
END IF;
|
|
END
|
|
$kfs_0017_sources_deletion_lifecycle_ck$;
|
|
|
|
DO $kfs_0017_document_assets_deletion_lifecycle_ck$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM "pg_constraint"
|
|
WHERE "conrelid" = 'document_assets'::regclass
|
|
AND "conname" = 'document_assets_deletion_lifecycle_ck'
|
|
) THEN
|
|
ALTER TABLE "document_assets"
|
|
ADD CONSTRAINT "document_assets_deletion_lifecycle_ck" CHECK (
|
|
"row_version" >= 1
|
|
AND (
|
|
("lifecycle_state" = 'active' AND "deletion_job_id" IS NULL AND "deleting_at" IS NULL)
|
|
OR
|
|
("lifecycle_state" = 'deleting' AND "deletion_job_id" IS NOT NULL AND "deleting_at" IS NOT NULL)
|
|
)
|
|
);
|
|
END IF;
|
|
END
|
|
$kfs_0017_document_assets_deletion_lifecycle_ck$;
|
|
|
|
-- Retrieval execution leases are durable, token-fenced evidence that a request may still be
|
|
-- reading the space. Deletion drains active leases before destructive cleanup and can reclaim
|
|
-- only rows whose heartbeat has expired.
|
|
CREATE TABLE IF NOT EXISTS "retrieval_execution_leases" (
|
|
"id" UUID PRIMARY KEY NOT NULL,
|
|
"tenant_id" VARCHAR(255) NOT NULL,
|
|
"knowledge_space_id" UUID NOT NULL,
|
|
"subject_id" TEXT NOT NULL,
|
|
"trace_id" UUID NOT NULL,
|
|
"lease_token" VARCHAR(128) NOT NULL,
|
|
"status" VARCHAR(16) NOT NULL,
|
|
"row_version" INTEGER NOT NULL,
|
|
"acquired_at" TIMESTAMPTZ NOT NULL,
|
|
"heartbeat_at" TIMESTAMPTZ NOT NULL,
|
|
"expires_at" TIMESTAMPTZ NOT NULL,
|
|
"updated_at" TIMESTAMPTZ NOT NULL,
|
|
CONSTRAINT "retrieval_execution_leases_state_ck" CHECK (
|
|
"status" IN ('active', 'released', 'expired')
|
|
AND "row_version" >= 0
|
|
AND "heartbeat_at" >= "acquired_at"
|
|
AND "expires_at" > "heartbeat_at"
|
|
AND "updated_at" >= "acquired_at"
|
|
),
|
|
CONSTRAINT "retrieval_execution_leases_space_fk"
|
|
FOREIGN KEY ("tenant_id", "knowledge_space_id")
|
|
REFERENCES "knowledge_spaces" ("tenant_id", "id") ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS "retrieval_execution_leases_space_expiry_idx"
|
|
ON "retrieval_execution_leases" (
|
|
"tenant_id", "knowledge_space_id", "status", "expires_at", "id"
|
|
);
|
|
|
|
-- Jobs intentionally have no FK to any resource or permission snapshot that they delete. The
|
|
-- copied tenant/scope/requester fields are a durable authorization and audit record.
|
|
CREATE TABLE IF NOT EXISTS "deletion_jobs" (
|
|
"id" UUID PRIMARY KEY NOT NULL,
|
|
"tenant_id" VARCHAR(255) NOT NULL,
|
|
"knowledge_space_id" UUID NOT NULL,
|
|
"target_type" VARCHAR(32) NOT NULL,
|
|
"target_id" UUID NOT NULL,
|
|
"target_revision" INTEGER NOT NULL,
|
|
"delete_mode" VARCHAR(16) NOT NULL,
|
|
"requested_by_subject_id" VARCHAR(255) NOT NULL,
|
|
"permission_snapshot_id" UUID NOT NULL,
|
|
"permission_snapshot_revision" INTEGER NOT NULL,
|
|
"access_channel" VARCHAR(16) NOT NULL,
|
|
"api_key_id" UUID,
|
|
"api_key_revision" INTEGER,
|
|
"api_key_expires_at" TIMESTAMPTZ,
|
|
"idempotency_key" VARCHAR(512) NOT NULL,
|
|
"request_fingerprint" CHAR(64) NOT NULL,
|
|
"name_challenge_digest" CHAR(64),
|
|
"checkpoint" VARCHAR(32) NOT NULL,
|
|
"scan_phase" VARCHAR(64),
|
|
"scan_cursor" VARCHAR(1024),
|
|
"inventory_complete" BOOLEAN NOT NULL,
|
|
"run_state" VARCHAR(16) NOT NULL,
|
|
"active_slot" INTEGER,
|
|
"execution_attempts" INTEGER NOT NULL,
|
|
"max_execution_attempts" INTEGER NOT NULL,
|
|
"retry_at" TIMESTAMPTZ,
|
|
"worker_id" VARCHAR(255),
|
|
"lease_token" UUID,
|
|
"lease_expires_at" TIMESTAMPTZ,
|
|
"heartbeat_at" TIMESTAMPTZ,
|
|
"queue_job_id" VARCHAR(255),
|
|
"last_error_code" VARCHAR(64),
|
|
"last_error_message" TEXT,
|
|
"row_version" INTEGER NOT NULL,
|
|
"created_at" TIMESTAMPTZ NOT NULL,
|
|
"updated_at" TIMESTAMPTZ NOT NULL,
|
|
"started_at" TIMESTAMPTZ,
|
|
"completed_at" TIMESTAMPTZ,
|
|
CONSTRAINT "deletion_jobs_target_ck" CHECK (
|
|
"target_type" IN ('knowledge_space', 'source', 'document_asset')
|
|
AND (
|
|
("target_type" = 'source' AND "delete_mode" IN ('keep', 'cascade') AND "name_challenge_digest" IS NULL)
|
|
OR
|
|
("target_type" = 'knowledge_space' AND "delete_mode" = 'cascade' AND "name_challenge_digest" IS NOT NULL)
|
|
OR
|
|
("target_type" = 'document_asset' AND "delete_mode" = 'cascade' AND "name_challenge_digest" IS NULL)
|
|
)
|
|
),
|
|
CONSTRAINT "deletion_jobs_checkpoint_ck" CHECK (
|
|
"checkpoint" IN (
|
|
'requested', 'quiescing', 'deleting_objects', 'deleting_derived_data',
|
|
'deleting_primary_data', 'completed'
|
|
)
|
|
),
|
|
CONSTRAINT "deletion_jobs_run_state_ck" CHECK (
|
|
"run_state" IN ('dispatch_pending', 'queued', 'running', 'retry_wait', 'succeeded', 'failed', 'canceled')
|
|
),
|
|
CONSTRAINT "deletion_jobs_access_channel_ck" CHECK (
|
|
"access_channel" IN ('interactive', 'service_api', 'mcp', 'agent')
|
|
),
|
|
CONSTRAINT "deletion_jobs_api_key_binding_ck" CHECK (
|
|
(
|
|
"api_key_id" IS NULL
|
|
AND "api_key_revision" IS NULL
|
|
AND "api_key_expires_at" IS NULL
|
|
)
|
|
OR (
|
|
"api_key_id" IS NOT NULL
|
|
AND "api_key_revision" >= 1
|
|
AND "access_channel" = 'service_api'
|
|
)
|
|
),
|
|
CONSTRAINT "deletion_jobs_positive_ck" CHECK (
|
|
"target_revision" >= 1
|
|
AND "permission_snapshot_revision" >= 1
|
|
AND "row_version" >= 1
|
|
AND "execution_attempts" >= 0
|
|
AND "max_execution_attempts" >= 1
|
|
AND "execution_attempts" <= "max_execution_attempts"
|
|
AND ("active_slot" IS NULL OR "active_slot" = 1)
|
|
),
|
|
CONSTRAINT "deletion_jobs_lifecycle_ck" CHECK (
|
|
(
|
|
"run_state" IN ('dispatch_pending', 'queued', 'running', 'retry_wait', 'failed')
|
|
AND "active_slot" = 1
|
|
AND "completed_at" IS NULL
|
|
)
|
|
OR (
|
|
"run_state" IN ('succeeded', 'canceled')
|
|
AND "active_slot" IS NULL
|
|
AND "completed_at" IS NOT NULL
|
|
)
|
|
),
|
|
CONSTRAINT "deletion_jobs_completion_ck" CHECK (
|
|
("run_state" = 'succeeded' AND "checkpoint" = 'completed')
|
|
OR ("run_state" <> 'succeeded' AND "checkpoint" <> 'completed')
|
|
),
|
|
CONSTRAINT "deletion_jobs_retry_ck" CHECK (
|
|
("run_state" = 'retry_wait' AND "retry_at" IS NOT NULL)
|
|
OR ("run_state" <> 'retry_wait' AND "retry_at" IS NULL)
|
|
),
|
|
CONSTRAINT "deletion_jobs_lease_ck" CHECK (
|
|
(
|
|
"run_state" = 'running'
|
|
AND "worker_id" IS NOT NULL
|
|
AND "lease_token" IS NOT NULL
|
|
AND "lease_expires_at" IS NOT NULL
|
|
AND "heartbeat_at" IS NOT NULL
|
|
)
|
|
OR (
|
|
"run_state" <> 'running'
|
|
AND "worker_id" IS NULL
|
|
AND "lease_token" IS NULL
|
|
AND "lease_expires_at" IS NULL
|
|
AND "heartbeat_at" IS NULL
|
|
)
|
|
)
|
|
);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS "deletion_jobs_idempotency_uq"
|
|
ON "deletion_jobs" ("tenant_id", "idempotency_key");
|
|
CREATE UNIQUE INDEX IF NOT EXISTS "deletion_jobs_target_active_uq"
|
|
ON "deletion_jobs" (
|
|
"tenant_id", "knowledge_space_id", "target_type", "target_id", "active_slot"
|
|
);
|
|
CREATE INDEX IF NOT EXISTS "deletion_jobs_claim_idx"
|
|
ON "deletion_jobs" ("run_state", "retry_at", "lease_expires_at", "created_at", "id");
|
|
CREATE INDEX IF NOT EXISTS "deletion_jobs_scope_history_idx"
|
|
ON "deletion_jobs" ("tenant_id", "knowledge_space_id", "created_at", "id");
|
|
CREATE INDEX IF NOT EXISTS "deletion_jobs_requester_provenance_idx"
|
|
ON "deletion_jobs" (
|
|
"tenant_id", "knowledge_space_id", "requested_by_subject_id",
|
|
"api_key_id", "api_key_revision", "created_at", "id"
|
|
);
|
|
|
|
-- Tombstones have no FK, including no FK back to the job, so the permanent no-republish fence
|
|
-- survives resource deletion and independent job retention.
|
|
CREATE TABLE IF NOT EXISTS "deletion_tombstones" (
|
|
"id" UUID PRIMARY KEY NOT NULL,
|
|
"deletion_job_id" UUID NOT NULL,
|
|
"tenant_id" VARCHAR(255) NOT NULL,
|
|
"knowledge_space_id" UUID NOT NULL,
|
|
"target_type" VARCHAR(32) NOT NULL,
|
|
"target_id" UUID NOT NULL,
|
|
"target_revision" INTEGER NOT NULL,
|
|
"state" VARCHAR(16) NOT NULL,
|
|
"row_version" INTEGER NOT NULL,
|
|
"created_at" TIMESTAMPTZ NOT NULL,
|
|
"completed_at" TIMESTAMPTZ,
|
|
CONSTRAINT "deletion_tombstones_target_ck" CHECK (
|
|
"target_type" IN ('knowledge_space', 'source', 'document_asset')
|
|
),
|
|
CONSTRAINT "deletion_tombstones_state_ck" CHECK (
|
|
("state" = 'active' AND "completed_at" IS NULL)
|
|
OR ("state" = 'completed' AND "completed_at" IS NOT NULL)
|
|
),
|
|
CONSTRAINT "deletion_tombstones_positive_ck" CHECK (
|
|
"target_revision" >= 1 AND "row_version" >= 1
|
|
)
|
|
);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS "deletion_tombstones_target_uq"
|
|
ON "deletion_tombstones" ("tenant_id", "target_type", "target_id");
|
|
CREATE INDEX IF NOT EXISTS "deletion_tombstones_space_target_idx"
|
|
ON "deletion_tombstones" ("tenant_id", "knowledge_space_id", "target_type", "target_id");
|
|
CREATE INDEX IF NOT EXISTS "deletion_tombstones_job_idx"
|
|
ON "deletion_tombstones" ("deletion_job_id", "id");
|
|
|
|
CREATE TABLE IF NOT EXISTS "deletion_job_items" (
|
|
"id" UUID PRIMARY KEY NOT NULL,
|
|
"deletion_job_id" UUID NOT NULL,
|
|
"ordinal" BIGINT NOT NULL,
|
|
"kind" VARCHAR(32) NOT NULL,
|
|
"resource_id" UUID,
|
|
"object_key" TEXT,
|
|
"credential_ref" TEXT,
|
|
"cache_key" TEXT,
|
|
"payload_digest" CHAR(64) NOT NULL,
|
|
"idempotency_key" VARCHAR(512) NOT NULL,
|
|
"status" VARCHAR(16) NOT NULL,
|
|
"attempts" INTEGER NOT NULL,
|
|
"max_attempts" INTEGER NOT NULL,
|
|
"next_attempt_at" TIMESTAMPTZ,
|
|
"last_error_code" VARCHAR(64),
|
|
"last_error_message" TEXT,
|
|
"row_version" INTEGER NOT NULL,
|
|
"created_at" TIMESTAMPTZ NOT NULL,
|
|
"updated_at" TIMESTAMPTZ NOT NULL,
|
|
"completed_at" TIMESTAMPTZ,
|
|
"redacted_at" TIMESTAMPTZ,
|
|
CONSTRAINT "deletion_job_items_kind_ck" CHECK (
|
|
"kind" IN ('object', 'secret_ref', 'cache_key', 'document_cascade', 'document_detach')
|
|
),
|
|
CONSTRAINT "deletion_job_items_status_ck" CHECK (
|
|
"status" IN ('pending', 'retry_wait', 'completed', 'dead')
|
|
),
|
|
CONSTRAINT "deletion_job_items_positive_ck" CHECK (
|
|
"ordinal" >= 0 AND "attempts" >= 0 AND "max_attempts" >= 1
|
|
AND "attempts" <= "max_attempts" AND "row_version" >= 1
|
|
),
|
|
CONSTRAINT "deletion_job_items_retry_ck" CHECK (
|
|
("status" = 'retry_wait' AND "next_attempt_at" IS NOT NULL)
|
|
OR ("status" <> 'retry_wait' AND "next_attempt_at" IS NULL)
|
|
),
|
|
CONSTRAINT "deletion_job_items_terminal_ck" CHECK (
|
|
("status" IN ('completed', 'dead') AND "completed_at" IS NOT NULL)
|
|
OR ("status" IN ('pending', 'retry_wait') AND "completed_at" IS NULL)
|
|
),
|
|
CONSTRAINT "deletion_job_items_payload_ck" CHECK (
|
|
(
|
|
"kind" = 'object' AND "credential_ref" IS NULL AND "cache_key" IS NULL
|
|
AND (
|
|
("status" = 'completed' AND "object_key" IS NULL AND "redacted_at" IS NOT NULL)
|
|
OR ("status" <> 'completed' AND "object_key" IS NOT NULL AND "redacted_at" IS NULL)
|
|
)
|
|
)
|
|
OR (
|
|
"kind" = 'secret_ref' AND "object_key" IS NULL AND "cache_key" IS NULL
|
|
AND (
|
|
("status" = 'completed' AND "credential_ref" IS NULL AND "redacted_at" IS NOT NULL)
|
|
OR ("status" <> 'completed' AND "credential_ref" IS NOT NULL AND "redacted_at" IS NULL)
|
|
)
|
|
)
|
|
OR (
|
|
"kind" = 'cache_key' AND "object_key" IS NULL AND "credential_ref" IS NULL
|
|
AND (
|
|
("status" = 'completed' AND "cache_key" IS NULL AND "redacted_at" IS NOT NULL)
|
|
OR ("status" <> 'completed' AND "cache_key" IS NOT NULL AND "redacted_at" IS NULL)
|
|
)
|
|
)
|
|
OR (
|
|
"kind" IN ('document_cascade', 'document_detach')
|
|
AND "resource_id" IS NOT NULL AND "object_key" IS NULL
|
|
AND "credential_ref" IS NULL AND "cache_key" IS NULL AND "redacted_at" IS NULL
|
|
)
|
|
),
|
|
FOREIGN KEY ("deletion_job_id") REFERENCES "deletion_jobs" ("id") ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS "deletion_job_items_idempotency_uq"
|
|
ON "deletion_job_items" ("deletion_job_id", "idempotency_key");
|
|
CREATE UNIQUE INDEX IF NOT EXISTS "deletion_job_items_ordinal_uq"
|
|
ON "deletion_job_items" ("deletion_job_id", "ordinal");
|
|
CREATE INDEX IF NOT EXISTS "deletion_job_items_work_idx"
|
|
ON "deletion_job_items" (
|
|
"deletion_job_id", "status", "next_attempt_at", "ordinal", "id"
|
|
);
|
|
CREATE INDEX IF NOT EXISTS "deletion_job_items_resource_idx"
|
|
ON "deletion_job_items" ("deletion_job_id", "kind", "resource_id", "id");
|
|
|
|
CREATE TABLE IF NOT EXISTS "deletion_outbox" (
|
|
"id" UUID PRIMARY KEY NOT NULL,
|
|
"deletion_job_id" UUID NOT NULL,
|
|
"delivery_revision" INTEGER NOT NULL,
|
|
"event_type" VARCHAR(32) NOT NULL,
|
|
"schema_version" INTEGER NOT NULL,
|
|
"idempotency_key" VARCHAR(512) NOT NULL,
|
|
"request_idempotency_key" VARCHAR(512) NOT NULL,
|
|
"request_fingerprint" CHAR(64) NOT NULL,
|
|
"payload" JSONB NOT NULL,
|
|
"status" VARCHAR(16) NOT NULL,
|
|
"available_at" TIMESTAMPTZ NOT NULL,
|
|
"dispatch_attempts" INTEGER NOT NULL,
|
|
"locked_by" VARCHAR(255),
|
|
"locked_until" TIMESTAMPTZ,
|
|
"lock_token" UUID,
|
|
"queue_job_id" VARCHAR(255),
|
|
"last_error" TEXT,
|
|
"delivered_at" TIMESTAMPTZ,
|
|
"created_at" TIMESTAMPTZ NOT NULL,
|
|
"updated_at" TIMESTAMPTZ NOT NULL,
|
|
CONSTRAINT "deletion_outbox_event_ck" CHECK ("event_type" = 'deletion.job'),
|
|
CONSTRAINT "deletion_outbox_schema_ck" CHECK ("schema_version" = 1),
|
|
CONSTRAINT "deletion_outbox_status_ck" CHECK (
|
|
"status" IN ('pending', 'dispatching', 'dispatched', 'leased', 'completed', 'canceled', 'dead')
|
|
),
|
|
CONSTRAINT "deletion_outbox_positive_ck" CHECK (
|
|
"delivery_revision" >= 1 AND "dispatch_attempts" >= 0
|
|
),
|
|
CONSTRAINT "deletion_outbox_lock_ck" CHECK (
|
|
("lock_token" IS NULL AND "locked_by" IS NULL AND "locked_until" IS NULL)
|
|
OR ("lock_token" IS NOT NULL AND "locked_by" IS NOT NULL AND "locked_until" IS NOT NULL)
|
|
),
|
|
FOREIGN KEY ("deletion_job_id") REFERENCES "deletion_jobs" ("id") ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS "deletion_outbox_idempotency_uq"
|
|
ON "deletion_outbox" ("idempotency_key");
|
|
CREATE UNIQUE INDEX IF NOT EXISTS "deletion_outbox_job_delivery_uq"
|
|
ON "deletion_outbox" ("deletion_job_id", "delivery_revision");
|
|
CREATE UNIQUE INDEX IF NOT EXISTS "deletion_outbox_job_request_uq"
|
|
ON "deletion_outbox" ("deletion_job_id", "request_idempotency_key");
|
|
CREATE INDEX IF NOT EXISTS "deletion_outbox_claim_idx"
|
|
ON "deletion_outbox" ("status", "available_at", "locked_until", "id");
|
|
|
|
-- Every manual retry has immutable actor provenance. This is intentionally separate from
|
|
-- deletion_jobs: an owner rescue must never overwrite the original deletion requester, and there
|
|
-- is deliberately no FK to the permission snapshot or target space because both may be deleted.
|
|
CREATE TABLE IF NOT EXISTS "deletion_retry_audits" (
|
|
"id" UUID PRIMARY KEY NOT NULL,
|
|
"deletion_job_id" UUID NOT NULL,
|
|
"outbox_id" UUID NOT NULL,
|
|
"tenant_id" VARCHAR(255) NOT NULL,
|
|
"knowledge_space_id" UUID NOT NULL,
|
|
"retry_authority" VARCHAR(32) NOT NULL,
|
|
"actor_subject_id" VARCHAR(255) NOT NULL,
|
|
"permission_snapshot_id" UUID NOT NULL,
|
|
"permission_snapshot_revision" INTEGER NOT NULL,
|
|
"access_channel" VARCHAR(16) NOT NULL,
|
|
"api_key_id" UUID,
|
|
"api_key_revision" INTEGER,
|
|
"api_key_expires_at" TIMESTAMPTZ,
|
|
"request_idempotency_key" VARCHAR(512) NOT NULL,
|
|
"request_fingerprint" CHAR(64) NOT NULL,
|
|
"created_at" TIMESTAMPTZ NOT NULL,
|
|
CONSTRAINT "deletion_retry_audits_authority_ck" CHECK (
|
|
"retry_authority" IN ('original_requester', 'interactive_owner_rescue')
|
|
),
|
|
CONSTRAINT "deletion_retry_audits_access_channel_ck" CHECK (
|
|
"access_channel" IN ('interactive', 'service_api', 'mcp', 'agent')
|
|
),
|
|
CONSTRAINT "deletion_retry_audits_positive_ck" CHECK (
|
|
"permission_snapshot_revision" >= 1
|
|
),
|
|
CONSTRAINT "deletion_retry_audits_api_key_binding_ck" CHECK (
|
|
(
|
|
"api_key_id" IS NULL
|
|
AND "api_key_revision" IS NULL
|
|
AND "api_key_expires_at" IS NULL
|
|
)
|
|
OR (
|
|
"api_key_id" IS NOT NULL
|
|
AND "api_key_revision" >= 1
|
|
AND "access_channel" = 'service_api'
|
|
)
|
|
),
|
|
CONSTRAINT "deletion_retry_audits_owner_rescue_ck" CHECK (
|
|
"retry_authority" <> 'interactive_owner_rescue'
|
|
OR (
|
|
"access_channel" = 'interactive'
|
|
AND "api_key_id" IS NULL
|
|
AND "api_key_revision" IS NULL
|
|
AND "api_key_expires_at" IS NULL
|
|
)
|
|
),
|
|
FOREIGN KEY ("deletion_job_id") REFERENCES "deletion_jobs" ("id") ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS "deletion_retry_audits_job_request_uq"
|
|
ON "deletion_retry_audits" ("deletion_job_id", "request_idempotency_key");
|
|
CREATE UNIQUE INDEX IF NOT EXISTS "deletion_retry_audits_outbox_uq"
|
|
ON "deletion_retry_audits" ("outbox_id");
|
|
CREATE INDEX IF NOT EXISTS "deletion_retry_audits_actor_idx"
|
|
ON "deletion_retry_audits" (
|
|
"tenant_id", "knowledge_space_id", "actor_subject_id", "created_at", "id"
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS "knowledge_spaces_lifecycle_idx"
|
|
ON "knowledge_spaces" ("tenant_id", "lifecycle_state", "updated_at", "id");
|
|
CREATE INDEX IF NOT EXISTS "sources_deletion_job_idx"
|
|
ON "sources" ("knowledge_space_id", "deletion_job_id", "id");
|
|
CREATE INDEX IF NOT EXISTS "document_assets_lifecycle_idx"
|
|
ON "document_assets" ("knowledge_space_id", "lifecycle_state", "source_id", "version", "id");
|
|
CREATE INDEX IF NOT EXISTS "page_index_manifests_document_idx"
|
|
ON "page_index_manifests" ("document_asset_id", "publication_generation_id", "id");
|
|
|
|
-- Agent workspace snapshots contain command logs and opaque metadata that cannot be attributed to
|
|
-- one document after the fact. Persist exact creator authorization and support whole-space
|
|
-- invalidation so every API replica observes deletion immediately.
|
|
CREATE TABLE IF NOT EXISTS "agent_workspace_snapshots" (
|
|
"id" UUID PRIMARY KEY NOT NULL,
|
|
"tenant_id" VARCHAR(255) NOT NULL,
|
|
"knowledge_space_id" UUID NOT NULL,
|
|
"subject_id" VARCHAR(255) NOT NULL,
|
|
"access_channel" VARCHAR(16) NOT NULL,
|
|
"permission_snapshot_id" UUID NOT NULL,
|
|
"permission_snapshot_revision" INTEGER NOT NULL,
|
|
"permission_scopes" JSONB NOT NULL,
|
|
"fingerprint" VARCHAR(80) NOT NULL,
|
|
"payload" JSONB NOT NULL,
|
|
"invalidated_at" TIMESTAMPTZ,
|
|
"invalidation_reason" VARCHAR(64),
|
|
"created_at" TIMESTAMPTZ NOT NULL,
|
|
CONSTRAINT "agent_workspace_snapshots_channel_ck" CHECK (
|
|
"access_channel" IN ('interactive', 'service_api', 'mcp', 'agent')
|
|
),
|
|
CONSTRAINT "agent_workspace_snapshots_revision_ck" CHECK (
|
|
"permission_snapshot_revision" >= 1
|
|
),
|
|
CONSTRAINT "agent_workspace_snapshots_invalidation_ck" CHECK (
|
|
("invalidated_at" IS NULL AND "invalidation_reason" IS NULL)
|
|
OR ("invalidated_at" IS NOT NULL AND "invalidation_reason" IS NOT NULL)
|
|
),
|
|
FOREIGN KEY ("tenant_id", "knowledge_space_id")
|
|
REFERENCES "knowledge_spaces" ("tenant_id", "id") ON DELETE CASCADE
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS "agent_workspace_snapshots_tenant_lookup_idx"
|
|
ON "agent_workspace_snapshots" ("tenant_id", "id", "invalidated_at");
|
|
CREATE INDEX IF NOT EXISTS "agent_workspace_snapshots_space_cleanup_idx"
|
|
ON "agent_workspace_snapshots" (
|
|
"tenant_id", "knowledge_space_id", "invalidated_at", "id"
|
|
);
|