- migrations 0002-0012 split by domain (foundation/instances/content/identity/ sources/assets/policy/execution/agent/ops/organization): 41 tables, uuid pk, created_at/updated_at + touch trigger, text+CHECK status sets, pgcrypto/pg_trgm - key constraints: source_release(site_id, site_release_id), observation(feed_id, guid), external_identity accepted partial unique, download_task(instance_id, torrent_hash), one active acquisition_plan per slot, one active path_binding per entity/season, asset_location root+path uniqueness, organization_record change_seq/change_xid cursor - secret table (ciphertext/nonce/kdf) + secret_ref columns; outbox dispatch fields (ADR-002) - src/types.ts generated from the migrated schema (scripts/gen-types.ts), drift-checked in tests; int8 -> number, date -> string parsers; migrator accepts migrationTableSchema - contracts: Row/Create/Update schemas per domain, public organization-record API (changeSeq cursor), GlobalSettings, CapabilityDescriptor + 14 capability IO schemas, typed SSE events, Api endpoint table - tests: migration up/down round trip on real PG in an isolated schema, constraint checks, contracts<->db column/enum sync, positive/negative parse for every exported schema Co-Authored-By: Claude Fable 5.1 <[email protected]>
100 lines
4.7 KiB
TypeScript
100 lines
4.7 KiB
TypeScript
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<any>): Promise<void> {
|
|
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<any>): Promise<void> {
|
|
await execDdl(db, dropTables(["path_binding", "asset_location", "asset_unit_coverage", "media_asset"]));
|
|
}
|