Files
dify/knowledge-fs/packages/database/migrations/0024_quality_control.postgres.sql
Jyong 4ee43b8afc chore: migrate knowledge-fs source tree
Import the committed KnowledgeFS snapshot dc4072ee302317145612087ce7440851dc329fd0 under knowledge-fs/ without its Git history, local IDE settings, or build artifacts.
2026-07-20 04:54:20 -04:00

366 lines
16 KiB
SQL

-- Knowledge Platform schema migration
-- Migration id: 0024_quality_control
-- Dialect: postgres
-- New-table-only DDL plus IF NOT EXISTS indexes keeps marker-loss replay safe.
CREATE UNIQUE INDEX IF NOT EXISTS "answer_traces_space_id_uq"
ON "answer_traces" ("knowledge_space_id", "id");
CREATE TABLE IF NOT EXISTS "quality_replay_runs" (
"id" UUID PRIMARY KEY NOT NULL,
"tenant_id" VARCHAR(255) NOT NULL,
"knowledge_space_id" UUID NOT NULL,
"idempotency_key" VARCHAR(255) NOT NULL,
"request_fingerprint" VARCHAR(71) NOT NULL,
"mode" VARCHAR(16) NOT NULL,
"state" VARCHAR(16) NOT NULL,
"requested_by_subject_id" VARCHAR(255) NOT NULL,
"access_channel" VARCHAR(16) NOT NULL,
"permission_snapshot_id" UUID NOT NULL,
"permission_snapshot_revision" INTEGER NOT NULL,
"required_permission_scope" JSONB NOT NULL,
"frozen_snapshot" JSONB NOT NULL,
"revision" INTEGER NOT NULL,
"attempt" INTEGER NOT NULL,
"lease_owner" VARCHAR(255),
"lease_token" UUID,
"lease_expires_at" TIMESTAMPTZ,
"error_message" TEXT,
"started_at" TIMESTAMPTZ,
"completed_at" TIMESTAMPTZ,
"created_at" TIMESTAMPTZ NOT NULL,
"updated_at" TIMESTAMPTZ NOT NULL,
CONSTRAINT "quality_replay_runs_state_ck" CHECK (
"mode" IN ('fast', 'research', 'deep')
AND "state" IN ('queued', 'running', 'passed', 'failed', 'canceled')
),
CONSTRAINT "quality_replay_runs_lease_ck" CHECK (
("state" = 'running' AND "lease_owner" IS NOT NULL AND "lease_token" IS NOT NULL
AND "lease_expires_at" IS NOT NULL AND "completed_at" IS NULL)
OR ("state" <> 'running' AND "lease_owner" IS NULL AND "lease_token" IS NULL
AND "lease_expires_at" IS NULL)
),
CONSTRAINT "quality_replay_runs_terminal_ck" CHECK (
("state" IN ('passed', 'failed', 'canceled') AND "completed_at" IS NOT NULL)
OR ("state" IN ('queued', 'running') AND "completed_at" IS NULL)
),
CONSTRAINT "quality_replay_runs_revision_ck" CHECK (
"revision" >= 1 AND "attempt" >= 0 AND "permission_snapshot_revision" >= 1
AND "request_fingerprint" ~ '^sha256:[a-f0-9]{64}$'
),
CONSTRAINT "quality_replay_runs_scope_json_ck"
CHECK (jsonb_typeof("required_permission_scope") = 'array'),
CONSTRAINT "quality_replay_runs_snapshot_json_ck"
CHECK (jsonb_typeof("frozen_snapshot") = 'object'),
FOREIGN KEY ("tenant_id", "knowledge_space_id")
REFERENCES "knowledge_spaces" ("tenant_id", "id") ON DELETE CASCADE,
FOREIGN KEY (
"knowledge_space_id", "permission_snapshot_id", "requested_by_subject_id", "access_channel"
) REFERENCES "knowledge_space_permission_snapshots" (
"knowledge_space_id", "id", "subject_id", "access_channel"
) ON DELETE RESTRICT
);
CREATE UNIQUE INDEX IF NOT EXISTS "quality_replay_runs_scope_id_uq"
ON "quality_replay_runs" ("tenant_id", "knowledge_space_id", "id");
CREATE UNIQUE INDEX IF NOT EXISTS "quality_replay_runs_idempotency_uq"
ON "quality_replay_runs" ("tenant_id", "knowledge_space_id", "idempotency_key");
CREATE INDEX IF NOT EXISTS "quality_replay_runs_scope_created_idx"
ON "quality_replay_runs" ("tenant_id", "knowledge_space_id", "created_at" DESC, "id" DESC);
CREATE INDEX IF NOT EXISTS "quality_replay_runs_claim_idx"
ON "quality_replay_runs" ("state", "lease_expires_at", "created_at", "id");
CREATE TABLE IF NOT EXISTS "quality_replay_items" (
"id" UUID PRIMARY KEY NOT NULL,
"run_id" UUID NOT NULL,
"golden_question_id" UUID NOT NULL,
"ordinal" INTEGER NOT NULL,
"question" TEXT NOT NULL,
"expected_evidence_ids" JSONB NOT NULL,
"state" VARCHAR(16) NOT NULL,
"result" JSONB,
"trace_id" UUID,
"created_at" TIMESTAMPTZ NOT NULL,
"updated_at" TIMESTAMPTZ NOT NULL,
CONSTRAINT "quality_replay_items_state_ck" CHECK (
"ordinal" >= 1 AND "state" IN ('queued', 'running', 'passed', 'failed', 'canceled')
),
CONSTRAINT "quality_replay_items_expected_json_ck"
CHECK (jsonb_typeof("expected_evidence_ids") = 'array'),
CONSTRAINT "quality_replay_items_result_json_ck"
CHECK ("result" IS NULL OR jsonb_typeof("result") = 'object'),
FOREIGN KEY ("run_id") REFERENCES "quality_replay_runs" ("id") ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS "quality_replay_items_run_ordinal_uq"
ON "quality_replay_items" ("run_id", "ordinal");
CREATE UNIQUE INDEX IF NOT EXISTS "quality_replay_items_run_golden_uq"
ON "quality_replay_items" ("run_id", "golden_question_id");
CREATE TABLE IF NOT EXISTS "quality_replay_outbox" (
"id" UUID PRIMARY KEY NOT NULL,
"run_id" UUID NOT NULL,
"delivery_revision" INTEGER NOT NULL,
"event_type" VARCHAR(64) NOT NULL,
"delivery_state" VARCHAR(16) NOT NULL,
"attempt" INTEGER NOT NULL,
"lease_owner" VARCHAR(255),
"lease_token" UUID,
"lease_expires_at" TIMESTAMPTZ,
"delivered_at" TIMESTAMPTZ,
"created_at" TIMESTAMPTZ NOT NULL,
"updated_at" TIMESTAMPTZ NOT NULL,
CONSTRAINT "quality_replay_outbox_state_ck" CHECK (
"delivery_revision" >= 1
AND "delivery_state" IN ('pending', 'claimed', 'delivered') AND "attempt" >= 0
AND (("delivery_state" = 'claimed' AND "lease_owner" IS NOT NULL
AND "lease_token" IS NOT NULL AND "lease_expires_at" IS NOT NULL
AND "delivered_at" IS NULL)
OR ("delivery_state" <> 'claimed' AND "lease_owner" IS NULL
AND "lease_token" IS NULL AND "lease_expires_at" IS NULL))
AND (("delivery_state" = 'delivered' AND "delivered_at" IS NOT NULL)
OR ("delivery_state" <> 'delivered' AND "delivered_at" IS NULL))
),
FOREIGN KEY ("run_id") REFERENCES "quality_replay_runs" ("id") ON DELETE CASCADE
);
ALTER TABLE "quality_replay_outbox"
ADD COLUMN IF NOT EXISTS "delivery_revision" INTEGER;
WITH ranked_delivery AS (
SELECT "id", ROW_NUMBER() OVER (
PARTITION BY "run_id" ORDER BY "created_at", "id"
) AS "delivery_revision"
FROM "quality_replay_outbox"
)
UPDATE "quality_replay_outbox" AS outbox
SET "delivery_revision" = ranked_delivery."delivery_revision"
FROM ranked_delivery
WHERE outbox."id" = ranked_delivery."id" AND outbox."delivery_revision" IS NULL;
ALTER TABLE "quality_replay_outbox"
ALTER COLUMN "delivery_revision" SET NOT NULL;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'quality_replay_outbox_delivery_revision_ck'
AND conrelid = 'quality_replay_outbox'::regclass
) THEN
ALTER TABLE "quality_replay_outbox"
ADD CONSTRAINT "quality_replay_outbox_delivery_revision_ck"
CHECK ("delivery_revision" >= 1);
END IF;
END $$;
CREATE INDEX IF NOT EXISTS "quality_replay_outbox_claim_idx"
ON "quality_replay_outbox" ("delivery_state", "lease_expires_at", "created_at", "id");
CREATE UNIQUE INDEX IF NOT EXISTS "quality_replay_outbox_run_delivery_uq"
ON "quality_replay_outbox" ("run_id", "delivery_revision");
CREATE INDEX IF NOT EXISTS "quality_replay_outbox_run_idx"
ON "quality_replay_outbox" ("run_id", "delivery_state", "id");
CREATE TABLE IF NOT EXISTS "quality_bad_cases" (
"id" UUID PRIMARY KEY NOT NULL,
"tenant_id" VARCHAR(255) NOT NULL,
"knowledge_space_id" UUID NOT NULL,
"trace_id" UUID NOT NULL,
"status" VARCHAR(16) NOT NULL,
"reason" TEXT NOT NULL,
"tags" JSONB NOT NULL,
"replay_run_id" UUID,
"actor_subject_id" VARCHAR(255) NOT NULL,
"revision" INTEGER NOT NULL,
"required_permission_scope" JSONB NOT NULL,
"created_at" TIMESTAMPTZ NOT NULL,
"updated_at" TIMESTAMPTZ NOT NULL,
CONSTRAINT "quality_bad_cases_state_ck" CHECK (
"status" IN ('open', 'replaying', 'fixed', 'dismissed') AND "revision" >= 1
AND ("status" <> 'replaying' OR "replay_run_id" IS NOT NULL)
),
CONSTRAINT "quality_bad_cases_tags_json_ck" CHECK (jsonb_typeof("tags") = 'array'),
CONSTRAINT "quality_bad_cases_scope_json_ck"
CHECK (jsonb_typeof("required_permission_scope") = 'array'),
FOREIGN KEY ("tenant_id", "knowledge_space_id")
REFERENCES "knowledge_spaces" ("tenant_id", "id") ON DELETE CASCADE,
CONSTRAINT "quality_bad_cases_trace_scope_fk"
FOREIGN KEY ("knowledge_space_id", "trace_id")
REFERENCES "answer_traces" ("knowledge_space_id", "id") ON DELETE CASCADE,
FOREIGN KEY ("tenant_id", "knowledge_space_id", "replay_run_id")
REFERENCES "quality_replay_runs" ("tenant_id", "knowledge_space_id", "id") ON DELETE RESTRICT
);
CREATE INDEX IF NOT EXISTS "quality_bad_cases_scope_status_idx"
ON "quality_bad_cases" ("tenant_id", "knowledge_space_id", "status", "created_at" DESC, "id" DESC);
CREATE TABLE IF NOT EXISTS "quality_missing_evidence_reviews" (
"id" UUID PRIMARY KEY NOT NULL,
"tenant_id" VARCHAR(255) NOT NULL,
"knowledge_space_id" UUID NOT NULL,
"trace_id" UUID NOT NULL,
"item_key" VARCHAR(71) NOT NULL,
"status" VARCHAR(16) NOT NULL,
"reason" TEXT,
"actor_subject_id" VARCHAR(255) NOT NULL,
"revision" INTEGER NOT NULL,
"required_permission_scope" JSONB NOT NULL,
"created_at" TIMESTAMPTZ NOT NULL,
"updated_at" TIMESTAMPTZ NOT NULL,
CONSTRAINT "quality_missing_evidence_reviews_state_ck" CHECK (
"status" IN ('active', 'dismissed') AND "revision" >= 1
AND "item_key" ~ '^sha256:[a-f0-9]{64}$'
),
CONSTRAINT "quality_missing_evidence_reviews_scope_json_ck"
CHECK (jsonb_typeof("required_permission_scope") = 'array'),
FOREIGN KEY ("tenant_id", "knowledge_space_id")
REFERENCES "knowledge_spaces" ("tenant_id", "id") ON DELETE CASCADE,
CONSTRAINT "quality_missing_evidence_reviews_trace_scope_fk"
FOREIGN KEY ("knowledge_space_id", "trace_id")
REFERENCES "answer_traces" ("knowledge_space_id", "id") ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS "quality_missing_reviews_item_uq"
ON "quality_missing_evidence_reviews" (
"tenant_id", "knowledge_space_id", "trace_id", "item_key"
);
CREATE TABLE IF NOT EXISTS "quality_resource_history" (
"id" UUID PRIMARY KEY NOT NULL,
"tenant_id" VARCHAR(255) NOT NULL,
"knowledge_space_id" UUID NOT NULL,
"aggregate_type" VARCHAR(32) NOT NULL,
"aggregate_id" UUID NOT NULL,
"action" VARCHAR(32) NOT NULL,
"actor_subject_id" VARCHAR(255) NOT NULL,
"from_status" VARCHAR(16),
"to_status" VARCHAR(16) NOT NULL,
"reason" TEXT,
"revision" INTEGER NOT NULL,
"created_at" TIMESTAMPTZ NOT NULL,
CONSTRAINT "quality_resource_history_type_ck" CHECK (
"aggregate_type" IN ('bad-case', 'missing-evidence') AND "revision" >= 1
),
FOREIGN KEY ("tenant_id", "knowledge_space_id")
REFERENCES "knowledge_spaces" ("tenant_id", "id") ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS "quality_resource_history_revision_uq"
ON "quality_resource_history" (
"tenant_id", "knowledge_space_id", "aggregate_type", "aggregate_id", "revision"
);
-- Golden-question visibility is frozen from referenced evidence. Legacy rows intentionally retain
-- NULL provenance and therefore fail every public tenant/scope read.
ALTER TABLE "golden_questions"
ADD COLUMN IF NOT EXISTS "tenant_id" VARCHAR(255),
ADD COLUMN IF NOT EXISTS "required_permission_scope" JSONB;
ALTER TABLE "golden_questions"
DROP CONSTRAINT IF EXISTS "golden_questions_scope_json_ck",
ADD CONSTRAINT "golden_questions_scope_json_ck" CHECK (
("tenant_id" IS NULL AND "required_permission_scope" IS NULL)
OR ("tenant_id" IS NOT NULL AND "required_permission_scope" IS NOT NULL
AND jsonb_typeof("required_permission_scope") = 'array')
);
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'golden_questions_scope_fk'
AND conrelid = 'golden_questions'::regclass
) THEN
ALTER TABLE "golden_questions"
ADD CONSTRAINT "golden_questions_scope_fk"
FOREIGN KEY ("tenant_id", "knowledge_space_id")
REFERENCES "knowledge_spaces" ("tenant_id", "id") ON DELETE CASCADE;
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'quality_bad_cases_trace_scope_fk'
AND conrelid = 'quality_bad_cases'::regclass
) THEN
ALTER TABLE "quality_bad_cases"
ADD CONSTRAINT "quality_bad_cases_trace_scope_fk"
FOREIGN KEY ("knowledge_space_id", "trace_id")
REFERENCES "answer_traces" ("knowledge_space_id", "id") ON DELETE CASCADE;
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'quality_missing_evidence_reviews_trace_scope_fk'
AND conrelid = 'quality_missing_evidence_reviews'::regclass
) THEN
ALTER TABLE "quality_missing_evidence_reviews"
ADD CONSTRAINT "quality_missing_evidence_reviews_trace_scope_fk"
FOREIGN KEY ("knowledge_space_id", "trace_id")
REFERENCES "answer_traces" ("knowledge_space_id", "id") ON DELETE CASCADE;
END IF;
END $$;
DROP INDEX IF EXISTS "golden_questions_space_id_idx";
CREATE INDEX IF NOT EXISTS "golden_questions_space_id_idx"
ON "golden_questions" ("tenant_id", "knowledge_space_id", "id");
DROP INDEX IF EXISTS "golden_questions_space_created_idx";
CREATE INDEX IF NOT EXISTS "golden_questions_space_created_idx"
ON "golden_questions" ("tenant_id", "knowledge_space_id", "created_at", "id");
CREATE INDEX IF NOT EXISTS "failed_queries_space_created_idx"
ON "failed_queries" ("knowledge_space_id", "created_at", "id");
-- Legacy failed-query rows deliberately remain provenance-free and are fail-closed by every read.
-- New captures write the complete binding atomically; nullable columns keep the migration online.
ALTER TABLE "failed_queries"
ADD COLUMN IF NOT EXISTS "tenant_id" VARCHAR(255),
ADD COLUMN IF NOT EXISTS "requested_by_subject_id" VARCHAR(255),
ADD COLUMN IF NOT EXISTS "access_channel" VARCHAR(16),
ADD COLUMN IF NOT EXISTS "permission_snapshot_id" UUID,
ADD COLUMN IF NOT EXISTS "permission_snapshot_revision" INTEGER,
ADD COLUMN IF NOT EXISTS "required_permission_scope" JSONB,
ADD COLUMN IF NOT EXISTS "revision" INTEGER;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'failed_queries_permission_binding_ck'
AND conrelid = 'failed_queries'::regclass
) THEN
ALTER TABLE "failed_queries"
ADD CONSTRAINT "failed_queries_permission_binding_ck" CHECK (
("tenant_id" IS NULL AND "requested_by_subject_id" IS NULL
AND "access_channel" IS NULL AND "permission_snapshot_id" IS NULL
AND "permission_snapshot_revision" IS NULL
AND "required_permission_scope" IS NULL AND "revision" IS NULL)
OR ("tenant_id" IS NOT NULL AND "requested_by_subject_id" IS NOT NULL
AND "access_channel" IS NOT NULL
AND "access_channel" IN ('interactive', 'service_api', 'mcp', 'agent')
AND "permission_snapshot_id" IS NOT NULL
AND "permission_snapshot_revision" IS NOT NULL
AND "permission_snapshot_revision" >= 1
AND "required_permission_scope" IS NOT NULL
AND jsonb_typeof("required_permission_scope") = 'array'
AND "revision" IS NOT NULL AND "revision" >= 1)
);
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'failed_queries_scope_fk'
AND conrelid = 'failed_queries'::regclass
) THEN
ALTER TABLE "failed_queries"
ADD CONSTRAINT "failed_queries_scope_fk"
FOREIGN KEY ("tenant_id", "knowledge_space_id")
REFERENCES "knowledge_spaces" ("tenant_id", "id") ON DELETE CASCADE;
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'failed_queries_permission_snapshot_fk'
AND conrelid = 'failed_queries'::regclass
) THEN
ALTER TABLE "failed_queries"
ADD CONSTRAINT "failed_queries_permission_snapshot_fk"
FOREIGN KEY ("tenant_id", "knowledge_space_id", "permission_snapshot_id",
"requested_by_subject_id", "access_channel")
REFERENCES "knowledge_space_permission_snapshots"
("tenant_id", "knowledge_space_id", "id", "subject_id", "access_channel")
ON DELETE RESTRICT;
END IF;
END $$;
CREATE INDEX IF NOT EXISTS "failed_queries_subject_created_idx"
ON "failed_queries" (
"tenant_id", "knowledge_space_id", "requested_by_subject_id", "created_at", "id"
);