优化数据库
This commit is contained in:
+102
-22
@@ -1,42 +1,122 @@
|
||||
import fs from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import process from 'node:process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import { closeDb, query, withTransaction } from './client.js'
|
||||
import { runner, type RunnerOption } from 'node-pg-migrate'
|
||||
import type { ClientConfig } from 'pg'
|
||||
|
||||
import { runtimeConfig } from '../config/runtime.js'
|
||||
import { closeDb, query } from './client.js'
|
||||
|
||||
const CURRENT_DIR = path.dirname(fileURLToPath(import.meta.url))
|
||||
const MIGRATIONS_DIR = path.join(CURRENT_DIR, 'migrations')
|
||||
const MIGRATIONS_TABLE = 'schema_migrations'
|
||||
const SHORT_NUMERIC_PREFIX_MESSAGE = /^Can't determine timestamp for \d+$/
|
||||
|
||||
export async function runDatabaseMigrations() {
|
||||
const files = fs.readdirSync(MIGRATIONS_DIR)
|
||||
.filter((name) => name.endsWith('.sql'))
|
||||
.sort()
|
||||
await ensureNodePgMigrateMetadataCompatibility()
|
||||
|
||||
await runner({
|
||||
databaseUrl: createMigrationDatabaseConfig(),
|
||||
dir: MIGRATIONS_DIR,
|
||||
direction: 'up',
|
||||
migrationsTable: MIGRATIONS_TABLE,
|
||||
migrationsSchema: 'public',
|
||||
schema: 'public',
|
||||
singleTransaction: true,
|
||||
checkOrder: true,
|
||||
logger: {
|
||||
debug: () => undefined,
|
||||
info: (message) => {
|
||||
console.info(`[db:migrate] ${message}`)
|
||||
},
|
||||
warn: (message) => {
|
||||
console.warn(`[db:migrate] ${message}`)
|
||||
},
|
||||
error: (message) => {
|
||||
if (SHORT_NUMERIC_PREFIX_MESSAGE.test(message)) {
|
||||
return
|
||||
}
|
||||
console.error(`[db:migrate] ${message}`)
|
||||
},
|
||||
},
|
||||
} satisfies RunnerOption)
|
||||
|
||||
await syncLegacyMigrationColumns()
|
||||
}
|
||||
|
||||
async function ensureNodePgMigrateMetadataCompatibility() {
|
||||
await query(`
|
||||
CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||
CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
filename TEXT NOT NULL UNIQUE,
|
||||
name VARCHAR(255),
|
||||
run_on TIMESTAMPTZ,
|
||||
filename TEXT,
|
||||
applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
)
|
||||
`)
|
||||
|
||||
const appliedRows = await query('SELECT filename FROM schema_migrations')
|
||||
const applied = new Set(appliedRows.rows.map((row) => String(row.filename)))
|
||||
await query(`
|
||||
ALTER TABLE ${MIGRATIONS_TABLE}
|
||||
ADD COLUMN IF NOT EXISTS name VARCHAR(255),
|
||||
ADD COLUMN IF NOT EXISTS run_on TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS filename TEXT,
|
||||
ADD COLUMN IF NOT EXISTS applied_at TIMESTAMPTZ
|
||||
`)
|
||||
|
||||
for (const filename of files) {
|
||||
if (applied.has(filename)) {
|
||||
continue
|
||||
}
|
||||
await query(`
|
||||
ALTER TABLE ${MIGRATIONS_TABLE}
|
||||
ALTER COLUMN filename DROP NOT NULL,
|
||||
ALTER COLUMN applied_at SET DEFAULT NOW()
|
||||
`)
|
||||
|
||||
const sql = fs.readFileSync(path.join(MIGRATIONS_DIR, filename), 'utf8')
|
||||
await withTransaction(async (client) => {
|
||||
await client.query(sql)
|
||||
await client.query(
|
||||
'INSERT INTO schema_migrations (filename, applied_at) VALUES ($1, NOW())',
|
||||
[filename],
|
||||
)
|
||||
})
|
||||
await query(`
|
||||
UPDATE ${MIGRATIONS_TABLE}
|
||||
SET
|
||||
name = COALESCE(
|
||||
NULLIF(name, ''),
|
||||
NULLIF(REGEXP_REPLACE(COALESCE(filename, ''), '\\.[^.]+$', ''), ''),
|
||||
CONCAT('legacy_', id)
|
||||
),
|
||||
run_on = COALESCE(run_on, applied_at, NOW())
|
||||
WHERE name IS NULL
|
||||
OR name = ''
|
||||
OR run_on IS NULL
|
||||
`)
|
||||
|
||||
await query(`
|
||||
ALTER TABLE ${MIGRATIONS_TABLE}
|
||||
ALTER COLUMN name SET NOT NULL,
|
||||
ALTER COLUMN run_on SET NOT NULL
|
||||
`)
|
||||
|
||||
await query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_schema_migrations_name
|
||||
ON ${MIGRATIONS_TABLE}(name)
|
||||
`)
|
||||
}
|
||||
|
||||
async function syncLegacyMigrationColumns() {
|
||||
await query(`
|
||||
UPDATE ${MIGRATIONS_TABLE}
|
||||
SET
|
||||
filename = COALESCE(NULLIF(filename, ''), CONCAT(name, '.sql')),
|
||||
applied_at = COALESCE(applied_at, run_on, NOW())
|
||||
WHERE filename IS NULL
|
||||
OR filename = ''
|
||||
OR applied_at IS NULL
|
||||
`)
|
||||
}
|
||||
|
||||
function createMigrationDatabaseConfig(): ClientConfig {
|
||||
const connectionString = String(runtimeConfig.database?.url || '').trim()
|
||||
if (!connectionString) {
|
||||
throw new Error('DATABASE_URL 未配置,无法执行数据库迁移')
|
||||
}
|
||||
|
||||
return {
|
||||
connectionString,
|
||||
ssl: runtimeConfig.database?.ssl ? { rejectUnauthorized: false } : false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +130,6 @@ if (process.argv[1] && path.resolve(process.argv[1]) === currentFilePath) {
|
||||
.catch(async (error) => {
|
||||
console.error(error)
|
||||
await closeDb()
|
||||
process.exit(1)
|
||||
process.exitCode = 1
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user