minime/src/checkdb.ts
Tau eb276128c2 sql: Add "maintenance transaction" system
SQLite's ALTER TABLE functionality leaves a lot to be desired, so
in practice the way you alter SQLite schema is to create a new
table, migrate the data into it, then drop the old table. However,
this is potentially complicated by the presence of foreign key
constraints.

In order to resolve the foreign key constraint issue we add a
`maintenance()` method to the `DataSource` abstraction layer. This
operates similarly to the existing `transaction()` method, but the
SQLite implementation of this method defers enforcement of foreign
keys until the transaction is about to commit.
2021-02-03 23:43:38 +00:00

139 lines
3.1 KiB
TypeScript

import { readdirSync, readFileSync } from "fs";
import logger from "debug";
import { resolve } from "path";
import sql from "sql-bricks-postgres";
import { DataSource, Transaction } from "./sql";
const scripts = [
// Order matters! Later scripts reference schema instantiated earlier.
"meta.sql",
"aime.sql",
"cm.sql",
"idz.sql",
];
const debug = logger("app:checkdb");
const initPath = "./schema/init";
const migratePath = "./schema/migrate";
const migrateRx = new RegExp("^M([0-9]{4})-.+\\.sql$");
async function initdb(txn: Transaction): Promise<void> {
// Get latest schemaver from migration script filenames
const migrations = readdirSync(migratePath);
let schemaver = 0;
for (const filename of migrations) {
const captures = filename.match(migrateRx);
if (captures === null) {
continue;
}
const migrationNo = parseInt(captures[1]);
if (schemaver < migrationNo) {
schemaver = migrationNo;
}
}
// Run init scripts
debug("Initializing database");
for (const script of scripts) {
debug("Executing %s", script);
const scriptPath = resolve(initPath, script);
const scriptSql = readFileSync(scriptPath, "utf-8");
await txn.raw(scriptSql);
}
// Set up metadata table
const metaInsert = sql.insert("meta", { schemaver });
await txn.modify(metaInsert);
debug("Initialized new database to schema version %s", schemaver);
}
async function migratedb(
txn: Transaction,
schemaver: number
): Promise<number | undefined> {
const filenames = readdirSync(migratePath);
let lastMigrationNo: number | undefined;
filenames.sort();
for (const filename of filenames) {
const captures = filename.match(migrateRx);
if (captures === null) {
debug("Warning: Unexpected file %s in SQL migrations dir", filename);
continue;
}
const migrationNo = parseInt(captures[1]);
if (migrationNo <= schemaver) {
continue;
}
debug("Executing database upgrade: %s", filename);
const scriptPath = resolve(migratePath, filename);
const scriptSql = readFileSync(scriptPath, "utf-8");
await txn.raw(scriptSql);
lastMigrationNo = migrationNo;
}
if (lastMigrationNo !== undefined) {
const metaUpdate = sql.update("meta", { schemaver: lastMigrationNo });
await txn.modify(metaUpdate);
}
return lastMigrationNo;
}
export default async function checkdb(db: DataSource): Promise<void> {
let schemaver: number | undefined;
await db.maintenance(async txn => {
const stmt = sql.select("schemaver").from("meta");
try {
const row = await txn.fetchRow(stmt);
if (row !== undefined) {
schemaver = parseInt(row.schemaver!);
}
} catch (e) {
return await initdb(txn);
}
if (schemaver === undefined) {
throw new Error(
"Database corrupted: `meta` table singleton row is missing"
);
}
schemaver = await migratedb(txn, schemaver);
});
if (schemaver !== undefined) {
debug("Upgraded database to version %s", schemaver);
await db.vacuum();
debug("Post-upgrade housekeeping complete");
}
}