Initialize cold WAL bookkeeping for subprocess snapshots

This commit is contained in:
syntaxbullet
2026-09-08 16:31:58 +02:00
parent 27beb01144
commit 7c7a048b32
3 changed files with 38 additions and 3 deletions

View File

@@ -1,5 +1,7 @@
import { Database } from "bun:sqlite"; import { Database } from "bun:sqlite";
import { databasePath } from "../src/db/config"; import { databasePath } from "../src/db/config";
import { backupConfig, createBackup } from "../src/ops/backups"; import { backupConfig, createBackup } from "../src/ops/backups";
const db = new Database(databasePath, { readonly: true, strict: true }); // SQLite needs writable WAL bookkeeping when the first snapshot opens a cold database.
// Only snapshot SQL runs here; never create a missing source file.
const db = new Database(databasePath, { readwrite: true, create: false, strict: true });
try { console.log(createBackup(db, backupConfig(databasePath))); } finally { db.close(); } try { console.log(createBackup(db, backupConfig(databasePath))); } finally { db.close(); }

View File

@@ -7,7 +7,9 @@ const config = JSON.parse(process.argv[3]) as BackupConfig;
const latest = latestBackupTime(config); const latest = latestBackupTime(config);
if (latest && Date.now() - latest < config.intervalMs) console.log(JSON.stringify({ timestamp: latest, created: false })); if (latest && Date.now() - latest < config.intervalMs) console.log(JSON.stringify({ timestamp: latest, created: false }));
else { else {
const db = new Database(path, { readonly: true, strict: true }); // SQLite needs writable WAL bookkeeping when the first snapshot opens a cold database.
// Only snapshot SQL runs here; never create a missing source file.
const db = new Database(path, { readwrite: true, create: false, strict: true });
try { try {
db.exec('PRAGMA busy_timeout=5000'); db.exec('PRAGMA busy_timeout=5000');
createBackup(db, config); createBackup(db, config);

View File

@@ -1,6 +1,6 @@
import { expect, test } from "bun:test"; import { expect, test } from "bun:test";
import { Database } from "bun:sqlite"; import { Database } from "bun:sqlite";
import { mkdtempSync, mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs"; import { existsSync, mkdtempSync, mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
import { join } from "node:path"; import { join } from "node:path";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { fixture } from "../habits/test-fixture"; import { fixture } from "../habits/test-fixture";
@@ -116,3 +116,34 @@ test('scheduled backup ticks coalesce, expose failure and recovery, and stop adm
const disabled = startBackups('/tmp/test-only.sqlite', { ...config, enabled: false }, run); const disabled = startBackups('/tmp/test-only.sqlite', { ...config, enabled: false }, run);
await disabled.ready; expect(calls).toBe(2); disabled.stop(); await disabled.ready; expect(calls).toBe(2); disabled.stop();
}); });
test('the first scheduled snapshot initializes cold WAL bookkeeping without changing source rows', async () => {
const directory = mkdtempSync(join(tmpdir(), 'minabot-cold-wal-'));
const path = join(directory, 'live.sqlite'), f = fixture(path);
f.sqlite.exec('PRAGMA journal_mode=DELETE'); f.close();
const db = new Database(path, { readwrite: true, create: false, strict: true });
let runner: ReturnType<typeof startBackups> | undefined;
try {
db.exec('PRAGMA journal_mode=WAL');
expect(existsSync(`${path}-shm`)).toBe(false);
runner = startBackups(db, backupConfig(path, {}));
await runner.ready;
expect(runner.state.failed).toBe(false);
expect(runner.state.lastSuccess).toBeGreaterThan(0);
expect(db.query('SELECT count(*) AS n FROM users').get()).toEqual({ n: 2 });
expect(db.query('SELECT count(*) AS n FROM sessions').get()).toEqual({ n: 2 });
expect(db.query('SELECT total_changes() AS n').get()).toEqual({ n: 0 });
} finally { runner?.stop(); db.close(); rmSync(directory, { recursive: true, force: true }); }
});
test('the scheduled snapshot never creates a missing source database', async () => {
const directory = mkdtempSync(join(tmpdir(), 'minabot-missing-source-'));
const path = join(directory, 'missing.sqlite');
const runner = startBackups(path, backupConfig(path, {}));
try {
await runner.ready;
expect(runner.state.failed).toBe(true);
expect(existsSync(path)).toBe(false);
} finally { runner.stop(); rmSync(directory, { recursive: true, force: true }); }
});