import type { Kysely } from "kysely"; import { dropTables, entityTable, execDdl, oneOf } from "../src/migration-helpers.ts"; // 身份域:external_identity + identity_revision(REQ §7.2) // provider + entity_type + external_id 对 accepted 唯一;候选可并存但不能同时被当作已确认身份。 const IDENTITY_STATUS = ["accepted", "proposed", "conflicted", "retired"] as const; // biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型 export async function up(db: Kysely): Promise { await execDdl( db, ` ${entityTable( "external_identity", ` media_entity_id uuid references media_entity (id) on delete restrict, content_unit_id uuid references content_unit (id) on delete restrict, provider text not null check (provider ~ '^[a-z][a-z0-9_]*$'), entity_type text not null ${oneOf("entity_type", ["movie", "tv", "season", "episode", "collection", "person"])}, external_id text not null, status text not null default 'proposed' ${oneOf("status", IDENTITY_STATUS)}, evidence_level text not null default 'needs_review' ${oneOf("evidence_level", ["manual", "rule_confirmed", "multi_evidence", "needs_review"])}, evidence jsonb not null default '{}'::jsonb, locked boolean not null default false, revision integer not null default 1, valid_from timestamptz, valid_until timestamptz, verified_at timestamptz, verify_status text not null default 'unverified' ${oneOf("verify_status", ["unverified", "ok", "not_found", "type_mismatch", "error"])}, extra jsonb not null default '{}'::jsonb, constraint external_identity_target_check check (num_nonnulls(media_entity_id, content_unit_id) = 1)`, )} create unique index external_identity_accepted_key on external_identity (provider, entity_type, external_id) where status = 'accepted'; create unique index external_identity_entity_accepted_key on external_identity (media_entity_id, provider, entity_type) where status = 'accepted' and media_entity_id is not null; create unique index external_identity_unit_accepted_key on external_identity (content_unit_id, provider, entity_type) where status = 'accepted' and content_unit_id is not null; create index external_identity_lookup_idx on external_identity (provider, entity_type, external_id); create index external_identity_entity_idx on external_identity (media_entity_id) where media_entity_id is not null; create index external_identity_unit_idx on external_identity (content_unit_id) where content_unit_id is not null; ${entityTable( "identity_revision", ` external_identity_id uuid not null references external_identity (id) on delete cascade, revision integer not null, action text not null ${oneOf("action", ["created", "status_changed", "relinked", "evidence_updated", "locked", "unlocked"])}, from_status text ${oneOf("from_status", IDENTITY_STATUS)}, to_status text ${oneOf("to_status", IDENTITY_STATUS)}, before jsonb, after jsonb not null default '{}'::jsonb, actor_type text not null ${oneOf("actor_type", ["user", "agent", "system", "api_key"])}, actor_id text, reason text, constraint identity_revision_seq_key unique (external_identity_id, revision)`, )} `, ); } // biome-ignore lint/suspicious/noExplicitAny: 同上 export async function down(db: Kysely): Promise { await execDdl(db, dropTables(["identity_revision", "external_identity"])); }