- 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]>
124 lines
5.7 KiB
TypeScript
124 lines
5.7 KiB
TypeScript
import type { Kysely } from "kysely";
|
|
import { dropTables, entityTable, execDdl, oneOf } from "../src/migration-helpers.ts";
|
|
|
|
// 规则域:subscription、policy_revision、acquisition_slot、preference_tag、preference_suppression(REQ §6)
|
|
// 每个 Slot 独立状态;策略版本化(草案/发布/回滚);取消的偏好留抑制记录,Agent 不得据旧证据自动加回。
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: 迁移不依赖应用表类型
|
|
export async function up(db: Kysely<any>): Promise<void> {
|
|
await execDdl(
|
|
db,
|
|
`
|
|
${entityTable(
|
|
"policy_revision",
|
|
`
|
|
policy_key text not null,
|
|
revision integer not null check (revision >= 1),
|
|
kind text not null ${oneOf("kind", ["acquisition", "preference", "retention", "cleanup", "instance", "automation"])},
|
|
status text not null default 'draft' ${oneOf("status", ["draft", "published", "superseded", "rolled_back", "archived"])},
|
|
title text not null,
|
|
summary text,
|
|
scope jsonb not null default '{}'::jsonb,
|
|
rules jsonb not null default '{}'::jsonb,
|
|
preview jsonb,
|
|
based_on_revision_id uuid references policy_revision (id) on delete set null,
|
|
created_by text,
|
|
published_at timestamptz,
|
|
published_by text,
|
|
constraint policy_revision_key unique (policy_key, revision)`,
|
|
)}
|
|
create unique index policy_revision_published_key on policy_revision (policy_key) where status = 'published';
|
|
|
|
${entityTable(
|
|
"subscription",
|
|
`
|
|
media_entity_id uuid references media_entity (id) on delete restrict,
|
|
season_unit_id uuid references content_unit (id) on delete restrict,
|
|
title_query text,
|
|
media_kind text not null ${oneOf("media_kind", ["movie", "series", "variety"])},
|
|
mode text not null default 'continuous' ${oneOf("mode", ["once", "continuous"])},
|
|
season_scope text not null default 'following_seasons' ${oneOf("season_scope", ["current_season", "following_seasons"])},
|
|
origin text not null default 'user' ${oneOf("origin", ["user", "agent", "policy", "import"])},
|
|
priority text not null default 'tracking' ${oneOf("priority", ["immediate", "tracking", "new_work", "upgrade"])},
|
|
status text not null default 'active' ${oneOf("status", ["active", "paused", "completed", "cancelled"])},
|
|
policy_revision_id uuid references policy_revision (id) on delete set null,
|
|
params jsonb not null default '{}'::jsonb,
|
|
status_reason text,
|
|
last_checked_at timestamptz,
|
|
created_by text,
|
|
constraint subscription_target_check check (media_entity_id is not null or title_query is not null)`,
|
|
)}
|
|
create index subscription_status_idx on subscription (status);
|
|
create index subscription_entity_idx on subscription (media_entity_id) where media_entity_id is not null;
|
|
|
|
${entityTable(
|
|
"acquisition_slot",
|
|
`
|
|
media_entity_id uuid not null references media_entity (id) on delete restrict,
|
|
content_unit_id uuid references content_unit (id) on delete restrict,
|
|
subscription_id uuid references subscription (id) on delete set null,
|
|
policy_revision_id uuid references policy_revision (id) on delete set null,
|
|
name text not null,
|
|
status text not null default 'empty' ${oneOf("status", ["empty", "reserved", "downloading", "verifying", "satisfied", "upgrade_pending", "attention"])},
|
|
hard_conditions jsonb not null default '{}'::jsonb,
|
|
soft_preferences jsonb not null default '{}'::jsonb,
|
|
fallback jsonb not null default '{}'::jsonb,
|
|
upgrade_threshold jsonb not null default '{}'::jsonb,
|
|
completion jsonb not null default '{}'::jsonb,
|
|
distinct_group text,
|
|
require_distinct_asset boolean not null default false,
|
|
current_asset_id uuid references media_asset (id) on delete set null,
|
|
status_reason text,
|
|
last_evaluated_at timestamptz,
|
|
next_evaluate_at timestamptz,
|
|
revision integer not null default 1`,
|
|
)}
|
|
create unique index acquisition_slot_target_key on acquisition_slot (media_entity_id, content_unit_id, name) nulls not distinct;
|
|
create index acquisition_slot_status_idx on acquisition_slot (status);
|
|
create index acquisition_slot_next_evaluate_idx on acquisition_slot (next_evaluate_at) where next_evaluate_at is not null;
|
|
create index acquisition_slot_subscription_idx on acquisition_slot (subscription_id) where subscription_id is not null;
|
|
|
|
${entityTable(
|
|
"preference_tag",
|
|
`
|
|
label text not null,
|
|
kind text not null ${oneOf("kind", ["soft", "hard", "version", "target"])},
|
|
scope jsonb not null default '{}'::jsonb,
|
|
rule jsonb not null default '{}'::jsonb,
|
|
fingerprint text not null,
|
|
origin text not null default 'manual' ${oneOf("origin", ["manual", "conversation", "agent_inferred"])},
|
|
description text,
|
|
status text not null default 'active' ${oneOf("status", ["active", "disabled", "deleted"])},
|
|
revision integer not null default 1,
|
|
evidence jsonb not null default '{}'::jsonb,
|
|
policy_revision_id uuid references policy_revision (id) on delete set null`,
|
|
)}
|
|
create index preference_tag_status_idx on preference_tag (status);
|
|
create unique index preference_tag_fingerprint_live_key on preference_tag (fingerprint) where status <> 'deleted';
|
|
|
|
${entityTable(
|
|
"preference_suppression",
|
|
`
|
|
preference_tag_id uuid references preference_tag (id) on delete set null,
|
|
fingerprint text not null,
|
|
reason text,
|
|
evidence jsonb not null default '{}'::jsonb,
|
|
suppressed_by text not null,
|
|
active boolean not null default true,
|
|
lifted_at timestamptz,
|
|
lifted_by text,
|
|
constraint preference_suppression_lift_check check (active or lifted_at is not null)`,
|
|
)}
|
|
create index preference_suppression_active_idx on preference_suppression (fingerprint) where active;
|
|
`,
|
|
);
|
|
}
|
|
|
|
// biome-ignore lint/suspicious/noExplicitAny: 同上
|
|
export async function down(db: Kysely<any>): Promise<void> {
|
|
await execDdl(
|
|
db,
|
|
dropTables(["preference_suppression", "preference_tag", "acquisition_slot", "subscription", "policy_revision"]),
|
|
);
|
|
}
|