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:
+43
-16
@@ -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()
|
||||
|
||||
@@ -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 },
|
||||
]
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
declare module '*.sql' {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
Reference in New Issue
Block a user