feat(deploy): migrations 嵌入二进制,实现真单文件部署

- embed-migrations.ts:扫 ./drizzle/meta/_journal.json,生成 src/server/db/migrations.gen.ts,每条 SQL 通过 `import sql_<idx> from '../../../drizzle/<tag>.sql' with { type: 'text' }` 在 bun build --compile 时被静态嵌入二进制
- migrate.ts 重写:runtime 用 createHash('sha256') 计算迁移哈希,仅用 db.execute(sql) + db.transaction() 公开 API 写入 drizzle.__drizzle_migrations 簿记表(不依赖 @internal 的 db.dialect/db.session)
- db:generate 链 db:embed,保证 SQL 改动总是同步到 migrations.gen.ts
- Dockerfile 删 COPY drizzle/,binary 是部署唯一 artifact
- 同步 README / AGENTS / biome.json
This commit is contained in:
2026-04-25 14:05:58 +08:00
parent e28fe9dc7b
commit 7e27640a26
13 changed files with 200 additions and 33 deletions
+43 -16
View File
@@ -1,30 +1,57 @@
import { existsSync } from 'node:fs'
import { defineCommand } from 'citty'
const MIGRATIONS_FOLDER = './drizzle'
const JOURNAL = `${MIGRATIONS_FOLDER}/meta/_journal.json`
export default defineCommand({
meta: {
name: 'migrate',
description: 'Apply pending database migrations from ./drizzle',
description: 'Apply pending database migrations',
},
async run() {
if (!existsSync(JOURNAL)) {
console.log(`No migrations found at ${MIGRATIONS_FOLDER} (run \`bun run db:generate\` to create some).`)
const [{ env }, { drizzle }, { sql }, { embeddedMigrations }, { createHash }] = await Promise.all([
import('@/env'),
import('drizzle-orm/postgres-js'),
import('drizzle-orm'),
import('@/server/db/migrations.gen'),
import('node:crypto'),
])
if (embeddedMigrations.length === 0) {
console.log('No migrations bundled into this binary.')
return
}
const [{ env }, { drizzle }, { migrate }] = await Promise.all([
import('@/env'),
import('drizzle-orm/postgres-js'),
import('drizzle-orm/postgres-js/migrator'),
])
const db = drizzle({ connection: { url: env.DATABASE_URL, max: 1 } })
const db = drizzle({ connection: { url: env.DATABASE_URL, max: 1, onnotice: () => {} } })
try {
console.log('Applying migrations...')
await migrate(db, { migrationsFolder: MIGRATIONS_FOLDER })
await db.execute(sql`CREATE SCHEMA IF NOT EXISTS "drizzle"`)
await db.execute(sql`
CREATE TABLE IF NOT EXISTS "drizzle"."__drizzle_migrations" (
id SERIAL PRIMARY KEY,
hash text NOT NULL,
created_at bigint
)
`)
const last = await db.execute<{ created_at: string | null }>(
sql`SELECT created_at FROM "drizzle"."__drizzle_migrations" ORDER BY created_at DESC LIMIT 1`,
)
const lastMillis = Number(last[0]?.created_at ?? 0)
const pending = embeddedMigrations.filter((m) => m.when > lastMillis)
if (pending.length === 0) {
console.log('Database is up to date.')
return
}
console.log(`Applying ${pending.length} migration(s)...`)
await db.transaction(async (tx) => {
for (const m of pending) {
for (const stmt of m.sql.split('--> statement-breakpoint')) {
await tx.execute(sql.raw(stmt))
}
const hash = createHash('sha256').update(m.sql).digest('hex')
await tx.execute(
sql`INSERT INTO "drizzle"."__drizzle_migrations" ("hash", "created_at") VALUES (${hash}, ${m.when})`,
)
}
})
console.log('Migrations applied.')
} finally {
await db.$client.end()
+8
View File
@@ -0,0 +1,8 @@
// AUTO-GENERATED by `bun run db:embed`. Do not edit.
import sql_0 from '../../../drizzle/0000_loving_thunderbird.sql' with { type: 'text' }
export type EmbeddedMigration = { tag: string; sql: string; when: number; breakpoints: boolean }
export const embeddedMigrations: readonly EmbeddedMigration[] = [
{ tag: '0000_loving_thunderbird', sql: sql_0, when: 1777096386609, breakpoints: true },
]
+4
View File
@@ -0,0 +1,4 @@
declare module '*.sql' {
const content: string
export default content
}