import type { Kysely } from "kysely"; import { dropTables, entityTable, execDdl, oneOf } from "../src/migration-helpers.ts"; // 文件域:media_asset、asset_unit_coverage、asset_location、path_binding(REQ §7.1、§8.2、§12.2) // 资产与位置分离;同一库根 + 有效相对路径不能被两个位置同时占用; // path_binding 一旦建立不因元数据刷新改变,只有显式整理操作可替换(旧绑定标 superseded)。 // biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型 export async function up(db: Kysely): Promise { await execDdl( db, ` ${entityTable( "media_asset", ` media_entity_id uuid references media_entity (id) on delete restrict, kind text not null ${oneOf("kind", ["video", "bluray_dir", "subtitle", "audio", "nfo", "image", "other"])}, parent_asset_id uuid references media_asset (id) on delete restrict, size_bytes bigint, content_hash text, hash_algorithm text, sample_digest text, media_info jsonb not null default '{}'::jsonb, delivery jsonb not null default '{}'::jsonb, content_edition text, source_release_id uuid references source_release (id) on delete set null, status text not null default 'active' ${oneOf("status", ["active", "replaced", "retired", "missing"])}, verified_at timestamptz`, )} create index media_asset_entity_idx on media_asset (media_entity_id) where media_entity_id is not null; create index media_asset_parent_idx on media_asset (parent_asset_id) where parent_asset_id is not null; ${entityTable( "asset_unit_coverage", ` media_asset_id uuid not null references media_asset (id) on delete cascade, content_unit_id uuid not null references content_unit (id) on delete restrict, coverage text not null default 'full' ${oneOf("coverage", ["full", "partial", "segment", "unknown"])}, segment text ${oneOf("segment", ["full", "upper", "middle", "lower", "unknown"])}, evidence jsonb not null default '{}'::jsonb, constraint asset_unit_coverage_key unique (media_asset_id, content_unit_id)`, )} create index asset_unit_coverage_unit_idx on asset_unit_coverage (content_unit_id); ${entityTable( "asset_location", ` media_asset_id uuid not null references media_asset (id) on delete cascade, location_kind text not null default 'library' ${oneOf("location_kind", ["library", "download_cache", "hardlink", "external"])}, library_root_id uuid references library_root (id) on delete restrict, downloader_instance_id uuid references downloader_instance (id) on delete set null, relative_path text not null, status text not null default 'available' ${oneOf("status", ["available", "unavailable", "suspected_missing", "confirmed_missing", "removed"])}, is_primary boolean not null default false, size_bytes bigint, inode bigint, device bigint, mtime timestamptz, sidecar_of_location_id uuid references asset_location (id) on delete set null, last_seen_at timestamptz, last_checked_at timestamptz, check_detail jsonb not null default '{}'::jsonb, constraint asset_location_library_root_check check (location_kind <> 'library' or library_root_id is not null)`, )} create unique index asset_location_root_path_key on asset_location (library_root_id, relative_path) where status <> 'removed' and library_root_id is not null; create index asset_location_asset_idx on asset_location (media_asset_id); create index asset_location_check_idx on asset_location (last_checked_at) where status <> 'removed'; ${entityTable( "path_binding", ` media_entity_id uuid not null references media_entity (id) on delete restrict, content_unit_id uuid references content_unit (id) on delete restrict, season_number integer, binding_kind text not null ${oneOf("binding_kind", ["entity", "season"])}, library_root_id uuid not null references library_root (id) on delete restrict, relative_path text not null, template_version text, status text not null default 'active' ${oneOf("status", ["active", "conflict", "superseded"])}, bound_at timestamptz not null default now(), bound_by text, superseded_by_id uuid references path_binding (id) on delete set null, note text, constraint path_binding_kind_check check ((binding_kind = 'season') = (season_number is not null))`, )} create index path_binding_entity_season_idx on path_binding (media_entity_id, season_number); create unique index path_binding_active_key on path_binding (media_entity_id, season_number) nulls not distinct where status = 'active'; create unique index path_binding_active_path_key on path_binding (library_root_id, relative_path) where status = 'active'; `, ); } // biome-ignore lint/suspicious/noExplicitAny: 同上 export async function down(db: Kysely): Promise { await execDdl(db, dropTables(["path_binding", "asset_location", "asset_unit_coverage", "media_asset"])); }