0000_crazy_gateway.sql 原本是 drizzle-kit pull 的产物,整份被 /* */ 包着、 可执行语句 0 条,所以任何新库的结构都只能先手工 psql 灌一遍 schema.sql 再打基线。 现在它的内容由 docs/specs/schema.sql(2026-08-07 的生产 pg_dump --schema-only) 机械转换而来:去掉 psql 专有指令 12 条、去掉 7 张 Django 遗留表及其索引外键 41 条, 保留 212 条语句、顺序不动。转换脚本入库在 docs/spikes/。 不含 Django 那 7 张表,是因为 meta/0000_snapshot.json 从来就没有它们(pull 当时 tablesFilter 滤掉了),不建它们才和快照一致;0002 那串 DROP ... IF EXISTS 在新库上 空转、在生产库上真删,两边跑同一串迁移落点相同。 改 0000 对生产库没有影响:migrator 只比 created_at、从不校验 hash,而生产库那行 baseline-0000-faked 早把它挡在门外了。 migrate.ts 配套:空库直接从 0000 建起(并自建 drizzle 记账表——原来这步由 drizzle 的 migrate() 顺手做掉);自举时不触发破坏性闸门,因为空库上没有数据可丢,拦下来只会逼 每个新环境都带一次 OJ2_ALLOW_DESTRUCTIVE,把这道闸训练成习惯动作。「有表但没基线」 仍然 exit 3。 验证:空库自举出来的结构,和「灌 schema.sql + 打基线 + 跑迁移」这条老路子跑出来的 结构,pg_dump --schema-only 逐字节一致(734 行,零差异)。 转换时踩到两个坑,都写进注释了:注释里不能出现 statement-breakpoint 的字面量 (readMigrationFiles 纯文本切分,会把注释从中间切开);过滤 Django 对象要看 public.X 形式的对象引用,不能扫语句字样——user 表有一列叫 auth_token。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
106 lines
5.0 KiB
TypeScript
106 lines
5.0 KiB
TypeScript
#!/usr/bin/env bun
|
||
// 把 docs/specs/schema.sql(生产库的 pg_dump --schema-only)转成可执行的基线迁移
|
||
// apps/api/src/db/0000_crazy_gateway.sql。
|
||
//
|
||
// 这是**一次性**的转换,产物已经入库。留着它是为了说清 0000 的出处、以及日后万一要
|
||
// 从一份新的生产 dump 重做基线时不用从头想一遍规则。日常改 schema 不要碰这里,
|
||
// 走 `bun run db:generate`。
|
||
//
|
||
// 跑法(仓库根):
|
||
// bun docs/spikes/build-baseline-migration.ts
|
||
//
|
||
// 产物是确定性的:同一份 schema.sql 跑出来的字节完全一样,改完可以直接 git diff 看。
|
||
import { readFileSync, writeFileSync } from "node:fs"
|
||
|
||
const SOURCE = "docs/specs/schema.sql"
|
||
const TARGET = "apps/api/src/db/0000_crazy_gateway.sql"
|
||
|
||
// drizzle 的语句分隔标记。故意不写成字面量常量之外的形式:`readMigrationFiles` 是纯文本
|
||
// 切分,这个串出现在哪里都会切,所以生成出来的文件的**注释里**绝不能带上它。
|
||
const BREAKPOINT = "--> statement-breakpoint"
|
||
|
||
// 判断一条语句是不是 Django 遗留物:只看它引用了哪些**对象**(`public.X` 形式),
|
||
// 不看语句里有没有出现这些字样。
|
||
//
|
||
// 踩过的坑:一开始扫整条语句里的 `auth_` / `django_` 字样,结果把 `user` 表整个滤掉了 ——
|
||
// 它有一列叫 `auth_token`。列名不带 `public.` 前缀,按对象引用来判就不会误伤。
|
||
//
|
||
// 覆盖到的形式:CREATE TABLE/SEQUENCE public.X、CREATE INDEX ... ON public.X、
|
||
// ALTER TABLE [ONLY] public.X、ALTER SEQUENCE public.X OWNED BY public.Y.id、
|
||
// 以及外键里的 REFERENCES public.Y —— 对象名全都跟在 `public.` 后面。
|
||
const OBJECT_REF = /public\."?([a-z_]+)"?/g
|
||
const DJANGO_PREFIX = ["auth_", "django_"]
|
||
|
||
function isDjango(stmt: string) {
|
||
return [...stmt.matchAll(OBJECT_REF)].some(([, name]) => DJANGO_PREFIX.some((p) => name.startsWith(p)))
|
||
}
|
||
|
||
const HEADER = `-- OJ2 的基线迁移:把一个空库建成新后端要的结构。
|
||
--
|
||
-- 这份文件**不是** \`drizzle-kit generate\` 的产物,也不该由它重新生成。原本这里是
|
||
-- \`drizzle-kit pull\` 吐出来的东西,整份被 /* */ 包着、一条可执行语句都没有,于是
|
||
-- 「空库没法靠迁移自举」——新环境、演练、别人接手,都得先手工 psql 灌一遍 schema.sql。
|
||
--
|
||
-- 现在的内容由 \`docs/specs/schema.sql\`(生产库 2026-08-07 的 pg_dump --schema-only)
|
||
-- 机械转换而来:去掉 psql 专有指令(\\restrict / SET / set_config)、去掉 7 张 Django
|
||
-- 遗留表及其索引与外键,其余原样保留、顺序不动,语句之间插上 drizzle 的分隔标记。
|
||
-- 转换脚本:\`docs/spikes/build-baseline-migration.ts\`。
|
||
--
|
||
-- 注意:那个分隔标记是纯文本切分,\`readMigrationFiles\` 不管它出现在哪里 —— 写进注释里
|
||
-- 一样会把文件切开。所以本文件的注释里不要出现它的字面量(我踩过一次,报错是
|
||
-- 「syntax error at or near "。"」,因为注释被从中间切断了)。
|
||
--
|
||
-- 为什么不含 Django 那 7 张表:\`meta/0000_snapshot.json\` 从来就没有它们(pull 当时用
|
||
-- tablesFilter 滤掉了),所以不建它们才和快照一致。0002 那条 DROP 全带 IF EXISTS,
|
||
-- 在新库上是空转,在生产库上才真删——两边跑同一串迁移,落点相同。
|
||
--
|
||
-- **改 schema 不要动这个文件**,走 \`bun run db:generate\` 生成新的迁移。
|
||
-- 生产库早已把 0000 标记成已执行(migrator 只比 created_at、不校验 hash),
|
||
-- 所以这份内容的任何改动都不会在生产库上重放。
|
||
`
|
||
|
||
const statements: string[] = []
|
||
let buffer: string[] = []
|
||
let droppedDjango = 0
|
||
let droppedNoise = 0
|
||
|
||
for (const line of readFileSync(SOURCE, "utf8").split("\n")) {
|
||
const trimmed = line.trim()
|
||
|
||
// 语句之外的行:空行、注释、psql 元命令(\restrict)直接跳过;
|
||
// SET / set_config 是 pg_dump 给自己用的会话设置,迁移里不需要。
|
||
if (buffer.length === 0) {
|
||
if (trimmed === "" || trimmed.startsWith("--") || trimmed.startsWith("\\")) continue
|
||
if (trimmed.startsWith("SET ") || trimmed.startsWith("SELECT pg_catalog.set_config")) {
|
||
droppedNoise++
|
||
continue
|
||
}
|
||
}
|
||
|
||
buffer.push(line)
|
||
|
||
// 按行尾分号断句。schema.sql 里句中出现分号的只有注释行,而注释行进不到这儿。
|
||
if (!trimmed.endsWith(";")) continue
|
||
|
||
const stmt = buffer.join("\n").trim()
|
||
buffer = []
|
||
|
||
if (isDjango(stmt)) {
|
||
droppedDjango++
|
||
continue
|
||
}
|
||
if (/^ALTER TABLE .* OWNER TO /.test(stmt) || stmt.startsWith("COMMENT ON")) {
|
||
droppedNoise++
|
||
continue
|
||
}
|
||
statements.push(stmt)
|
||
}
|
||
|
||
if (buffer.length > 0) throw new Error(`有没闭合的语句:${buffer[0]}`)
|
||
|
||
const body = statements.map((s) => s.replace(/;+$/, "")).join(`;\n${BREAKPOINT}\n`)
|
||
writeFileSync(TARGET, `${HEADER}\n${body};\n`)
|
||
|
||
console.log(`保留 ${statements.length} 条语句 | 滤掉 Django 相关 ${droppedDjango} 条、噪音 ${droppedNoise} 条`)
|
||
console.log(`已写入 ${TARGET}`)
|