79 lines
2.9 KiB
TypeScript
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)
|
|
}
|
|
}
|