Files
UHD_Media_Tool/packages/db/src/migrator.ts
T
rootandClaude Opus 5.5 f6063af12d fix(db): migrator introspects only the target schema
Kysely's Migrator checks for its tables via introspection.getTables(),
which reflects every schema and calls has_schema_privilege /
pg_get_serial_sequence per schema. When another test file drops its schema
at the same moment those functions fail with "schema … does not exist" —
the intermittent server PG test failures on the shared dev database.
createMigrator now hands the Migrator a proxy whose introspection only
looks at the migration schema (or current_schema()). A regression test
churns schemas while migrating; it fails on the old migrator.

Also samples EngineRunQuery's comma-separated status/triggeredBy in the
contracts schema test.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-28 17:15:04 +08:00

88 lines
3.9 KiB
TypeScript

import { promises as fs } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { type DatabaseIntrospector, type Kysely, sql, type TableMetadata } from "kysely";
import { FileMigrationProvider, type MigrationResultSet, Migrator } from "kysely/migration";
/** 迁移目录:packages/db/migrations,文件名 NNNN_name.ts,导出 up/down */
export const MIGRATIONS_DIR = fileURLToPath(new URL("../migrations", import.meta.url));
export interface MigratorOptions {
/**
* kysely_migration 表所在 schema。默认不指定(走 search_path)。
* 在非 public schema 里迁移(如测试隔离 schema)时必须显式指定,
* 否则 Kysely 会把 public 下已存在的迁移表当成当前库的迁移记录。
*/
migrationTableSchema?: string;
}
/**
* 只看一个 schema 的内省器。Kysely 迁移器判断迁移表是否存在时会调用 introspection.getTables(),
* 默认实现反射全库所有 schema,并对每个 schema 调 has_schema_privilege / pg_get_serial_sequence;
* 同一个库里别的进程恰好 drop schema 时,这两个函数会报 "schema … does not exist"(并行测试共用开发库时的偶发失败)。
* 迁移器只需要知道迁移表 / 锁表在不在目标 schema,这里只查目标 schema(未指定时取 current_schema())的表名。
*/
// biome-ignore lint/suspicious/noExplicitAny: 同上
function scopedIntrospector(db: Kysely<any>, schema: string | undefined): DatabaseIntrospector {
const getTables = async (): Promise<TableMetadata[]> => {
const rows = await sql<{ name: string; schema: string; kind: string }>`
select c.relname as name, n.nspname as schema, c.relkind as kind
from pg_catalog.pg_class c join pg_catalog.pg_namespace n on n.oid = c.relnamespace
where n.nspname = coalesce(${schema ?? null}::text, current_schema())
and c.relkind in ('r', 'v', 'p', 'f')`.execute(db);
return rows.rows.map((r) => ({
name: r.name,
schema: r.schema,
isView: r.kind === "v",
isForeign: r.kind === "f",
columns: [],
}));
};
return {
async getSchemas() {
const rows = await sql<{ name: string }>`
select nspname as name from pg_catalog.pg_namespace
where nspname = coalesce(${schema ?? null}::text, current_schema())`.execute(db);
return rows.rows;
},
getTables,
};
}
/** 把 db.introspection 换成只看目标 schema 的实现,其余成员原样转发(方法绑回原实例,Kysely 用了私有字段) */
// biome-ignore lint/suspicious/noExplicitAny: 同上
function withScopedIntrospection(db: Kysely<any>, schema: string | undefined): Kysely<any> {
const introspection = scopedIntrospector(db, schema);
return new Proxy(db, {
get(target, prop) {
if (prop === "introspection") return introspection;
const v = Reflect.get(target, prop, target);
return typeof v === "function" ? v.bind(target) : v;
},
});
}
// biome-ignore lint/suspicious/noExplicitAny: 迁移器不关心具体表类型
export function createMigrator(db: Kysely<any>, opts: MigratorOptions = {}): Migrator {
return new Migrator({
db: withScopedIntrospection(db, opts.migrationTableSchema),
provider: new FileMigrationProvider({ fs, path, migrationFolder: MIGRATIONS_DIR }),
...(opts.migrationTableSchema ? { migrationTableSchema: opts.migrationTableSchema } : {}),
});
}
/** 迁移到最新;失败时抛出 */
// biome-ignore lint/suspicious/noExplicitAny: 同上
export async function migrateToLatest(db: Kysely<any>, opts: MigratorOptions = {}): Promise<MigrationResultSet> {
const result = await createMigrator(db, opts).migrateToLatest();
if (result.error) throw result.error;
return result;
}
/** 清空 public schema(仅开发/测试用) */
// biome-ignore lint/suspicious/noExplicitAny: 同上
export async function resetSchema(db: Kysely<any>): Promise<void> {
await sql`drop schema if exists public cascade`.execute(db);
await sql`create schema public`.execute(db);
}