- 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]>
74 lines
3.4 KiB
TypeScript
74 lines
3.4 KiB
TypeScript
import type { Kysely } from "kysely";
|
|
import { dropTables, entityTable, execDdl, oneOf } from "../src/migration-helpers.ts";
|
|
|
|
// 内容域:本地作品 media_entity、内容单元 content_unit、别名 title_alias(REQ §3.1、§5.1、§5.4、§7.5)
|
|
// 本地 UUID 为主身份;外部 ID 与集号都不是唯一身份,编号可修订。
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
|
|
export async function up(db: Kysely<any>): Promise<void> {
|
|
await execDdl(
|
|
db,
|
|
`
|
|
${entityTable(
|
|
"media_entity",
|
|
`
|
|
kind text not null ${oneOf("kind", ["movie", "series", "variety", "collection", "other"])},
|
|
title text not null,
|
|
original_title text,
|
|
year integer,
|
|
status text not null default 'active' ${oneOf("status", ["provisional", "active", "merged", "archived"])},
|
|
merged_into_id uuid references media_entity (id) on delete restrict,
|
|
metadata jsonb not null default '{}'::jsonb,
|
|
field_locks text[] not null default '{}'::text[],
|
|
last_metadata_refresh_at timestamptz,
|
|
constraint media_entity_merged_check check ((status = 'merged') = (merged_into_id is not null))`,
|
|
)}
|
|
create index media_entity_kind_idx on media_entity (kind);
|
|
create index media_entity_merged_into_idx on media_entity (merged_into_id) where merged_into_id is not null;
|
|
|
|
${entityTable(
|
|
"content_unit",
|
|
`
|
|
media_entity_id uuid not null references media_entity (id) on delete restrict,
|
|
parent_unit_id uuid references content_unit (id) on delete restrict,
|
|
kind text not null ${oneOf("kind", ["season", "episode", "special", "part"])},
|
|
content_type text not null default 'main' ${oneOf("content_type", ["main", "extra", "behind_the_scenes", "performance_only", "preview", "reaction", "unknown"])},
|
|
segment text not null default 'full' ${oneOf("segment", ["full", "upper", "middle", "lower", "unknown"])},
|
|
season_number integer,
|
|
episode_number integer,
|
|
source_issue text,
|
|
air_date date,
|
|
display_order double precision,
|
|
title text,
|
|
status text not null default 'provisional' ${oneOf("status", ["provisional", "confirmed", "retired"])},
|
|
numbering_locked boolean not null default false,
|
|
metadata jsonb not null default '{}'::jsonb,
|
|
field_locks text[] not null default '{}'::text[]`,
|
|
)}
|
|
create index content_unit_entity_numbering_idx on content_unit (media_entity_id, season_number, episode_number);
|
|
create index content_unit_parent_idx on content_unit (parent_unit_id) where parent_unit_id is not null;
|
|
create index content_unit_air_date_idx on content_unit (media_entity_id, air_date) where air_date is not null;
|
|
|
|
${entityTable(
|
|
"title_alias",
|
|
`
|
|
media_entity_id uuid not null references media_entity (id) on delete cascade,
|
|
content_unit_id uuid references content_unit (id) on delete cascade,
|
|
alias text not null,
|
|
normalized text not null,
|
|
language text,
|
|
kind text not null default 'other' ${oneOf("kind", ["original", "translated", "short", "site_title", "romanized", "other"])},
|
|
source text not null ${oneOf("source", ["tmdb", "user", "site", "agent", "nfo", "import"])},
|
|
locked boolean not null default false`,
|
|
)}
|
|
create unique index title_alias_entity_normalized_idx on title_alias (media_entity_id, content_unit_id, normalized, kind) nulls not distinct;
|
|
create index title_alias_normalized_trgm_idx on title_alias using gin (normalized gin_trgm_ops);
|
|
`,
|
|
);
|
|
}
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: 同上
|
|
export async function down(db: Kysely<any>): Promise<void> {
|
|
await execDdl(db, dropTables(["title_alias", "content_unit", "media_entity"]));
|
|
}
|