Verify SQLite backups and restores

This commit is contained in:
2026-07-23 19:12:14 +02:00
parent 4d5eb22e66
commit 030f18aeb0
7 changed files with 205 additions and 22 deletions
+78
View File
@@ -0,0 +1,78 @@
import fs from "node:fs";
import path from "node:path";
import Database from "better-sqlite3";
export interface DatabaseVerification {
integrityCheck: "ok";
foreignKeyViolations: number;
}
function resolveDatabasePath(filename: string): string {
return path.resolve(filename);
}
export function verifyDatabaseFile(filename: string): DatabaseVerification {
const databasePath = resolveDatabasePath(filename);
const sqlite = new Database(databasePath, {
readonly: true,
fileMustExist: true,
});
try {
const integrityCheck = sqlite.pragma("integrity_check", {
simple: true,
});
if (integrityCheck !== "ok") {
throw new Error(
`SQLite integrity check failed for ${databasePath}: ${String(integrityCheck)}`
);
}
const foreignKeyCheck = sqlite.pragma("foreign_key_check") as unknown[];
const foreignKeyViolations = foreignKeyCheck.length;
if (foreignKeyViolations > 0) {
throw new Error(
`SQLite foreign-key check failed for ${databasePath}: ${foreignKeyViolations} violation(s)`
);
}
return {
integrityCheck: "ok",
foreignKeyViolations,
};
} finally {
sqlite.close();
}
}
export async function createVerifiedDatabaseBackup(
sourceFilename: string,
destinationFilename: string
): Promise<DatabaseVerification> {
const sourcePath = resolveDatabasePath(sourceFilename);
const destinationPath = resolveDatabasePath(destinationFilename);
if (sourcePath === destinationPath) {
throw new Error("Backup source and destination must be different files.");
}
if (!fs.existsSync(sourcePath)) {
throw new Error(`Database file not found: ${sourcePath}`);
}
if (fs.existsSync(destinationPath)) {
throw new Error(`Backup destination already exists: ${destinationPath}`);
}
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
const source = new Database(sourcePath, {
readonly: true,
fileMustExist: true,
});
try {
await source.backup(destinationPath);
} finally {
source.close();
}
return verifyDatabaseFile(destinationPath);
}