Files
MikrotikManager/backend/src/db/migrate.ts
T

79 lines
2.9 KiB
TypeScript

import { readdirSync, readFileSync } from "node:fs"
import { dirname, join } from "node:path"
import { fileURLToPath } from "node:url"
import type { Pool } from "pg"
import { PARTITION_WRITER_ADVISORY_KEY, PartitionWriteError } from "../services/partition-write-transaction.js"
const FIRST_MIGRATION = "0000_postgresql.sql"
function migrationsDir(): string {
const here = dirname(fileURLToPath(import.meta.url))
const candidates = [
join(here, "..", "..", "drizzle"),
join(process.cwd(), "drizzle"),
join(process.cwd(), "backend", "drizzle"),
]
for (const dir of candidates) {
try {
readFileSync(join(dir, FIRST_MIGRATION), "utf8")
return dir
} catch {
/* try next */
}
}
throw new Error("Не найден backend/drizzle/0000_postgresql.sql")
}
function migrationId(file: string): string {
return file.replace(/\.sql$/i, "")
}
export async function applySqlMigrations(pool: Pool): Promise<void> {
// Migration files can own BEGIN/COMMIT themselves. A session shared lease
// therefore spans every file on THIS checked-out backend, including schema
// discovery and marker writes; a lock on another pool backend is insufficient.
const client = await pool.connect()
let released = false, targetSqlSent = false
try {
await client.query("SET lock_timeout='10s'")
await client.query("SET statement_timeout='120s'")
await client.query("SELECT pg_advisory_lock_shared($1::bigint)", [PARTITION_WRITER_ADVISORY_KEY])
targetSqlSent = true
await client.query(`
CREATE TABLE IF NOT EXISTS schema_migrations (
id TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
)
`)
const dir = migrationsDir()
const files = readdirSync(dir)
.filter((f) => /^\d{4}_.+\.sql$/i.test(f))
.sort((a, b) => a.localeCompare(b))
const applied = new Set(
(await client.query<{ id: string }>(`SELECT id FROM schema_migrations`)).rows.map((r) => r.id),
)
for (const file of files) {
const id = migrationId(file)
if (applied.has(id)) continue
const sql = readFileSync(join(dir, file), "utf8")
await client.query(sql)
await client.query(
`INSERT INTO schema_migrations (id) VALUES ($1) ON CONFLICT (id) DO NOTHING`,
[id],
)
}
const unlocked = await client.query<{ unlocked: boolean }>("SELECT pg_advisory_unlock_shared($1::bigint) AS unlocked", [PARTITION_WRITER_ADVISORY_KEY])
if (unlocked.rows[0]?.unlocked !== true) throw new PartitionWriteError(targetSqlSent)
await client.query("RESET lock_timeout")
await client.query("RESET statement_timeout")
released = true; client.release(false)
} catch {
// A whole migration query may include several committed statements. Never
// send ROLLBACK or automatically repeat files after an uncertain boundary.
if (!released) { released = true; client.release(true) }
throw new PartitionWriteError(targetSqlSent)
} finally {
if (!released) client.release(true)
}
}