mirror of
https://github.com/djhackersdev/minime.git
synced 2026-07-08 04:54:09 -05:00
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.
This commit is contained in:
parent
6090a60467
commit
eb276128c2
|
|
@ -106,7 +106,7 @@ async function migratedb(
|
|||
export default async function checkdb(db: DataSource): Promise<void> {
|
||||
let schemaver: number | undefined;
|
||||
|
||||
await db.transaction(async txn => {
|
||||
await db.maintenance(async txn => {
|
||||
const stmt = sql.select("schemaver").from("meta");
|
||||
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,16 @@ export interface DataSource {
|
|||
*/
|
||||
transaction<T>(callback: (txn: Transaction) => Promise<T>): Promise<T>;
|
||||
|
||||
/**
|
||||
* Execute an async callback in the context of a "maintenance" transaction.
|
||||
*
|
||||
* This method should be used when altering the data store's schema. On
|
||||
* SQLite it will temporarily disable foreign key constraints (and then
|
||||
* re-enable and validate them immediately prior to committing the
|
||||
* transaction) so that tables may be dropped and re-created.
|
||||
*/
|
||||
maintenance<T>(callback: (txn: Transaction) => Promise<T>): Promise<T>;
|
||||
|
||||
/**
|
||||
* Perform housekeeping operations on the database's on-disk data structures.
|
||||
*
|
||||
|
|
|
|||
33
src/sql/sqlite.test.ts
Normal file
33
src/sql/sqlite.test.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { Transaction } from "./api";
|
||||
import { openSqlite } from "./sqlite";
|
||||
|
||||
async function violateFk(txn: Transaction) {
|
||||
await txn.raw("create table foo (id number primary key)");
|
||||
await txn.raw("create table bar (foo_id integer references foo(id))");
|
||||
await txn.raw("insert into bar (foo_id) values (1)");
|
||||
}
|
||||
|
||||
test("FK enforcement", async () => {
|
||||
const db = openSqlite(":memory:");
|
||||
const promise = db.transaction(violateFk);
|
||||
|
||||
expect(promise).rejects.toMatchObject({
|
||||
message: "FOREIGN KEY constraint failed",
|
||||
});
|
||||
});
|
||||
|
||||
test("FK violation detection", async () => {
|
||||
const db = openSqlite(":memory:");
|
||||
const promise = db.maintenance(violateFk);
|
||||
|
||||
expect(promise).rejects.toMatchObject({
|
||||
fkViolations: [
|
||||
{
|
||||
fkid: BigInt(0),
|
||||
parent: "foo",
|
||||
rowid: BigInt(1),
|
||||
table: "bar",
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
import Database, { ResultRow } from "@decafcode/sqlite";
|
||||
import { randomBytes } from "crypto";
|
||||
import logger from "debug";
|
||||
import * as sql from "sql-bricks-postgres";
|
||||
|
||||
import { DataSource, Row, Transaction } from "./api";
|
||||
import { Id } from "../model";
|
||||
|
||||
const debug = logger("app:sqlite");
|
||||
|
||||
// bless me father for i have sinned
|
||||
const fuFixup = new RegExp(" FOR UPDATE$");
|
||||
|
||||
|
|
@ -53,6 +56,12 @@ function _postprocess(obj: ResultRow): Row {
|
|||
return result;
|
||||
}
|
||||
|
||||
export class MaintenanceError extends Error {
|
||||
constructor(msg: string, public readonly fkViolations: any[]) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
|
||||
class SqliteTransaction implements Transaction {
|
||||
constructor(private readonly _db: Database) {}
|
||||
|
||||
|
|
@ -141,6 +150,41 @@ class SqliteDataSource implements DataSource {
|
|||
}
|
||||
}
|
||||
|
||||
async maintenance<T>(
|
||||
callback: (txn: Transaction) => Promise<T>
|
||||
): Promise<T> {
|
||||
const db = new Database(this._path);
|
||||
|
||||
try {
|
||||
db.exec("pragma foreign_keys=off");
|
||||
db.exec("begin");
|
||||
|
||||
const txn = new SqliteTransaction(db);
|
||||
const result = await callback(txn);
|
||||
|
||||
const fkViolations = db.prepare("pragma foreign_key_check").all();
|
||||
|
||||
if (fkViolations.length > 0) {
|
||||
debug("Foreign key violations: %O", fkViolations);
|
||||
|
||||
throw new MaintenanceError(
|
||||
"Maintenance failed, db unchanged. Consult 'app:sqlite' debug log.",
|
||||
fkViolations
|
||||
);
|
||||
}
|
||||
|
||||
db.exec("commit");
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
db.exec("rollback");
|
||||
|
||||
throw e;
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
|
||||
vacuum(): Promise<void> {
|
||||
const db = new Database(this._path);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user