mirror of
https://github.com/jlengrand/Maestro.git
synced 2026-03-10 00:21:21 +00:00
fix: checkpoint WAL before database backup to prevent data loss
fs.copyFileSync on a WAL-mode SQLite database can produce incomplete backups because committed data may still reside in the -wal file. Added safeBackupCopy() that runs PRAGMA wal_checkpoint(TRUNCATE) before copying, ensuring the .db file is self-contained.
This commit is contained in:
@@ -851,6 +851,60 @@ describe('Daily backup system', () => {
|
||||
expect(dailyBackupCalls).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WAL checkpoint before backup', () => {
|
||||
it('should checkpoint WAL before creating daily backup', async () => {
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
mockFsExistsSync.mockImplementation((p: unknown) => {
|
||||
if (typeof p === 'string' && p.includes(`daily.${today}`)) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
const { StatsDB } = await import('../../../main/stats');
|
||||
const db = new StatsDB();
|
||||
db.initialize();
|
||||
|
||||
// Should have called wal_checkpoint(TRUNCATE) before copyFileSync
|
||||
expect(mockDb.pragma).toHaveBeenCalledWith('wal_checkpoint(TRUNCATE)');
|
||||
});
|
||||
|
||||
it('should checkpoint WAL before creating manual backup', async () => {
|
||||
const { StatsDB } = await import('../../../main/stats');
|
||||
const db = new StatsDB();
|
||||
db.initialize();
|
||||
|
||||
mockDb.pragma.mockClear();
|
||||
db.backupDatabase();
|
||||
|
||||
expect(mockDb.pragma).toHaveBeenCalledWith('wal_checkpoint(TRUNCATE)');
|
||||
});
|
||||
|
||||
it('should call checkpoint before copyFileSync (correct ordering)', async () => {
|
||||
const callOrder: string[] = [];
|
||||
mockDb.pragma.mockImplementation((pragmaStr: string) => {
|
||||
if (pragmaStr === 'wal_checkpoint(TRUNCATE)') {
|
||||
callOrder.push('checkpoint');
|
||||
}
|
||||
if (pragmaStr === 'integrity_check') return [{ integrity_check: 'ok' }];
|
||||
return [{ user_version: 3 }];
|
||||
});
|
||||
mockFsCopyFileSync.mockImplementation(() => {
|
||||
callOrder.push('copy');
|
||||
});
|
||||
|
||||
const { StatsDB } = await import('../../../main/stats');
|
||||
const db = new StatsDB();
|
||||
db.initialize();
|
||||
|
||||
mockDb.pragma.mockClear();
|
||||
mockFsCopyFileSync.mockClear();
|
||||
callOrder.length = 0;
|
||||
|
||||
db.backupDatabase();
|
||||
|
||||
expect(callOrder).toEqual(['checkpoint', 'copy']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -321,6 +321,22 @@ export class StatsDB {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkpoint WAL to flush pending writes into the main database file,
|
||||
* then copy the database file to the destination path.
|
||||
*
|
||||
* Plain fs.copyFileSync on a WAL-mode database can produce an incomplete
|
||||
* copy because committed data may still reside in the -wal file.
|
||||
* PRAGMA wal_checkpoint(TRUNCATE) forces all WAL content into the main
|
||||
* file and resets the WAL, making the .db file self-contained.
|
||||
*/
|
||||
private safeBackupCopy(destPath: string): void {
|
||||
if (this.db) {
|
||||
this.db.pragma('wal_checkpoint(TRUNCATE)');
|
||||
}
|
||||
fs.copyFileSync(this.dbPath, destPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a backup of the current database file.
|
||||
*/
|
||||
@@ -333,7 +349,7 @@ export class StatsDB {
|
||||
const timestamp = Date.now();
|
||||
const backupPath = `${this.dbPath}.backup.${timestamp}`;
|
||||
|
||||
fs.copyFileSync(this.dbPath, backupPath);
|
||||
this.safeBackupCopy(backupPath);
|
||||
|
||||
logger.info(`Created database backup at ${backupPath}`, LOG_CONTEXT);
|
||||
return { success: true, backupPath };
|
||||
@@ -367,8 +383,8 @@ export class StatsDB {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create today's backup
|
||||
fs.copyFileSync(this.dbPath, dailyBackupPath);
|
||||
// Create today's backup (checkpoint WAL first so the copy is self-contained)
|
||||
this.safeBackupCopy(dailyBackupPath);
|
||||
logger.info(`Created daily backup: ${dailyBackupPath}`, LOG_CONTEXT);
|
||||
|
||||
// Rotate old backups (keep last 7 days)
|
||||
|
||||
Reference in New Issue
Block a user