refactor(db): 移除嵌入式迁移链路
This commit is contained in:
@@ -1,11 +0,0 @@
|
||||
import { defineConfig } from 'drizzle-kit'
|
||||
import { env } from './src/env'
|
||||
|
||||
export default defineConfig({
|
||||
out: './drizzle',
|
||||
schema: './src/server/db/schema/index.ts',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: {
|
||||
url: env.DATABASE_URL,
|
||||
},
|
||||
})
|
||||
@@ -1,7 +0,0 @@
|
||||
CREATE TABLE "todo" (
|
||||
"id" uuid PRIMARY KEY NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"title" text NOT NULL,
|
||||
"completed" boolean DEFAULT false NOT NULL
|
||||
);
|
||||
@@ -1,65 +0,0 @@
|
||||
{
|
||||
"id": "4ece5479-57bf-473d-b806-c1176c972e7f",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.todo": {
|
||||
"name": "todo",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"completed": {
|
||||
"name": "completed",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1777096386609,
|
||||
"tag": "0000_loving_thunderbird",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import { readFile, writeFile } from 'node:fs/promises'
|
||||
import { z } from 'zod'
|
||||
|
||||
const JOURNAL = './drizzle/meta/_journal.json'
|
||||
const OUTPUT = './src/server/db/migrations.gen.ts'
|
||||
|
||||
const journalEntrySchema = z.object({
|
||||
idx: z.number().int().nonnegative(),
|
||||
tag: z.string().regex(/^\d{4}_[a-z0-9_]+$/),
|
||||
when: z.number().int().nonnegative(),
|
||||
breakpoints: z.boolean(),
|
||||
})
|
||||
const journalSchema = z.object({ entries: z.array(journalEntrySchema).default([]) })
|
||||
|
||||
type JournalEntry = z.infer<typeof journalEntrySchema>
|
||||
|
||||
const readJournalEntries = async (): Promise<JournalEntry[]> => {
|
||||
if (!existsSync(JOURNAL)) {
|
||||
return []
|
||||
}
|
||||
const raw: unknown = JSON.parse(await readFile(JOURNAL, 'utf-8'))
|
||||
return journalSchema.parse(raw).entries.sort((a, b) => a.idx - b.idx)
|
||||
}
|
||||
|
||||
const main = async () => {
|
||||
const entries = await readJournalEntries()
|
||||
|
||||
const imports = entries
|
||||
.map((e) => `import sql_${e.idx} from '#drizzle/${e.tag}.sql' with { type: 'text' }`)
|
||||
.join('\n')
|
||||
|
||||
const arrayBody = entries.length
|
||||
? `[\n${entries.map((e) => ` { tag: '${e.tag}', sql: sql_${e.idx}, when: ${e.when}, breakpoints: ${e.breakpoints} },`).join('\n')}\n]`
|
||||
: '[]'
|
||||
|
||||
const out = `// AUTO-GENERATED by \`bun run db:embed\`. Do not edit.
|
||||
${imports ? `${imports}\n` : ''}
|
||||
export type EmbeddedMigration = { tag: string; sql: string; when: number; breakpoints: boolean }
|
||||
|
||||
export const embeddedMigrations: readonly EmbeddedMigration[] = ${arrayBody}
|
||||
`
|
||||
|
||||
await writeFile(OUTPUT, out)
|
||||
console.log(`✓ ${OUTPUT} (${entries.length} migration${entries.length === 1 ? '' : 's'})`)
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('❌', err instanceof Error ? err.message : err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -1,84 +0,0 @@
|
||||
import { defineCommand } from 'citty'
|
||||
|
||||
export default defineCommand({
|
||||
meta: {
|
||||
name: 'migrate',
|
||||
description: 'Apply pending database migrations',
|
||||
},
|
||||
async run() {
|
||||
const [{ env }, { drizzle }, { sql }, { embeddedMigrations }, { getLogger }] = await Promise.all([
|
||||
import('@/env'),
|
||||
import('drizzle-orm/postgres-js'),
|
||||
import('drizzle-orm'),
|
||||
import('@/server/db/migrations.gen'),
|
||||
import('@/server/logger'),
|
||||
])
|
||||
|
||||
const logger = getLogger(['cli', 'migrate'])
|
||||
|
||||
if (embeddedMigrations.length === 0) {
|
||||
logger.info('No migrations bundled into this binary.')
|
||||
return
|
||||
}
|
||||
|
||||
const sha256 = (s: string) => Bun.CryptoHasher.hash('sha256', s, 'hex')
|
||||
|
||||
const db = drizzle({
|
||||
connection: {
|
||||
url: env.DATABASE_URL,
|
||||
max: 1,
|
||||
onnotice: (n) => logger.debug('pg notice', { notice: n.message }),
|
||||
},
|
||||
})
|
||||
try {
|
||||
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 applied = await db.execute<{ hash: string; created_at: string | null }>(
|
||||
sql`SELECT hash, created_at FROM "drizzle"."__drizzle_migrations" ORDER BY created_at ASC`,
|
||||
)
|
||||
|
||||
// Reject schema drift: any applied migration whose embedded SQL has changed (or is missing) is fatal.
|
||||
for (const row of applied) {
|
||||
const when = Number(row.created_at)
|
||||
const m = embeddedMigrations.find((e) => e.when === when)
|
||||
if (!m) {
|
||||
throw new Error(`Applied migration when=${when} is not in this binary; do not roll back applied migrations.`)
|
||||
}
|
||||
if (sha256(m.sql) !== row.hash) {
|
||||
throw new Error(`Migration hash mismatch at when=${when}; do not edit migrations after they are applied.`)
|
||||
}
|
||||
}
|
||||
|
||||
const appliedWhens = new Set(applied.map((r) => Number(r.created_at)))
|
||||
const pending = embeddedMigrations.filter((m) => !appliedWhens.has(m.when))
|
||||
if (pending.length === 0) {
|
||||
logger.info('Database is up to date.')
|
||||
return
|
||||
}
|
||||
|
||||
logger.info('Applying {count} migration(s)...', { count: pending.length })
|
||||
await db.transaction(async (tx) => {
|
||||
for (const m of pending) {
|
||||
for (const rawStmt of m.sql.split('--> statement-breakpoint')) {
|
||||
const stmt = rawStmt.trim()
|
||||
if (!stmt) continue
|
||||
await tx.execute(sql.raw(stmt))
|
||||
}
|
||||
await tx.execute(
|
||||
sql`INSERT INTO "drizzle"."__drizzle_migrations" ("hash", "created_at") VALUES (${sha256(m.sql)}, ${m.when})`,
|
||||
)
|
||||
}
|
||||
})
|
||||
logger.info('Migrations applied.')
|
||||
} finally {
|
||||
await db.$client.end()
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -1,8 +0,0 @@
|
||||
// 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
@@ -1,4 +0,0 @@
|
||||
declare module '*.sql' {
|
||||
const content: string
|
||||
export default content
|
||||
}
|
||||
Reference in New Issue
Block a user