diff --git a/apps/server/scripts/migrate-dev-db.ts b/apps/server/scripts/migrate-dev-db.ts index 50d1b4e37909..38faef1f1cfd 100644 --- a/apps/server/scripts/migrate-dev-db.ts +++ b/apps/server/scripts/migrate-dev-db.ts @@ -210,6 +210,7 @@ const ensureNotInUse = Effect.fn("ensureDevDbNotInUse")(function* (databasePath: const checkpoint = yield* Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; yield* sql.unsafe("PRAGMA busy_timeout = 0").unprepared; + yield* sql.unsafe("PRAGMA checkpoint_fullfsync = ON").unprepared; yield* sql.unsafe("BEGIN IMMEDIATE").unprepared; yield* sql.unsafe("ROLLBACK").unprepared; return yield* sql.unsafe<{ busy: number }>("PRAGMA wal_checkpoint(TRUNCATE)").unprepared; @@ -483,6 +484,7 @@ export const runMigrateDevDb = Effect.fn("runMigrateDevDb")(function* ( // WAL does not survive VACUUM INTO; set it so first `vp run dev` finds // the database exactly as server boot would have left it. yield* sql.unsafe("PRAGMA journal_mode = WAL").unprepared; + yield* sql.unsafe("PRAGMA checkpoint_fullfsync = ON").unprepared; }).pipe( Effect.provide(NodeSqliteClient.layer({ filename: databasePath })), wrapPhase("compact", databasePath), diff --git a/apps/server/scripts/t3-sqlite-state.test.ts b/apps/server/scripts/t3-sqlite-state.test.ts index fa5471bfa8c4..95936af3e1a2 100644 --- a/apps/server/scripts/t3-sqlite-state.test.ts +++ b/apps/server/scripts/t3-sqlite-state.test.ts @@ -72,6 +72,26 @@ it.layer(NodeServices.layer)("t3-sqlite-state", (it) => { if (result.operation === "query") { assert.deepStrictEqual(result.rows, [{ id: 1, label: "existing" }]); } + + const checkpoint = yield* runSqliteState({ + operation: "query", + baseDir, + sql: "PRAGMA checkpoint_fullfsync", + }); + assert.equal(checkpoint.operation, "query"); + if (checkpoint.operation === "query") { + assert.deepStrictEqual(checkpoint.rows, [{ checkpoint_fullfsync: 1 }]); + } + + const fullfsync = yield* runSqliteState({ + operation: "query", + baseDir, + sql: "PRAGMA fullfsync", + }); + assert.equal(fullfsync.operation, "query"); + if (fullfsync.operation === "query") { + assert.deepStrictEqual(fullfsync.rows, [{ fullfsync: 0 }]); + } }), ); diff --git a/apps/server/scripts/t3-sqlite-state.ts b/apps/server/scripts/t3-sqlite-state.ts index 5e274588671f..a15d4994a4b3 100644 --- a/apps/server/scripts/t3-sqlite-state.ts +++ b/apps/server/scripts/t3-sqlite-state.ts @@ -201,6 +201,8 @@ export const runSqliteState = Effect.fn("runSqliteState")(function* ( const program = Effect.gen(function* () { const sql = yield* SqlClient.SqlClient; yield* sql.unsafe("PRAGMA busy_timeout = 5000").unprepared; + // This connection bypasses Sqlite.ts and can checkpoint state.sqlite. + yield* sql.unsafe("PRAGMA checkpoint_fullfsync = ON").unprepared; if (input.operation === "query") { const rows = yield* sql.unsafe(source).unprepared.pipe( diff --git a/apps/server/src/persistence/Layers/Sqlite.test.ts b/apps/server/src/persistence/Layers/Sqlite.test.ts index 0b64e4f7fdcb..0518ebaa61f0 100644 --- a/apps/server/src/persistence/Layers/Sqlite.test.ts +++ b/apps/server/src/persistence/Layers/Sqlite.test.ts @@ -64,3 +64,15 @@ it.effect("applies busy_timeout in the shared persistence setup", () => assert.equal(rows[0]?.timeout, 5000); }).pipe(Effect.provide(SqlitePersistenceMemory)), ); + +it.effect("uses F_FULLFSYNC for WAL checkpoints and leaves per-commit fullfsync off", () => + Effect.gen(function* () { + const sql = yield* SqlClient.SqlClient; + const checkpoint = yield* sql<{ + readonly checkpoint_fullfsync: number; + }>`PRAGMA checkpoint_fullfsync`; + const fullfsync = yield* sql<{ readonly fullfsync: number }>`PRAGMA fullfsync`; + assert.equal(checkpoint[0]?.checkpoint_fullfsync, 1); + assert.equal(fullfsync[0]?.fullfsync, 0); + }).pipe(Effect.provide(SqlitePersistenceMemory)), +); diff --git a/apps/server/src/persistence/Layers/Sqlite.ts b/apps/server/src/persistence/Layers/Sqlite.ts index 88342cbf1fad..e51e8f50becc 100644 --- a/apps/server/src/persistence/Layers/Sqlite.ts +++ b/apps/server/src/persistence/Layers/Sqlite.ts @@ -15,6 +15,9 @@ const setup = Layer.effectDiscard( yield* sql`PRAGMA busy_timeout = 5000;`; yield* sql`PRAGMA foreign_keys = ON;`; yield* sql`PRAGMA journal_mode = WAL;`; + // Checkpoint syncs use F_FULLFSYNC where the platform provides it. Ordinary + // commit sync stays on fsync(); PRAGMA fullfsync is left off. + yield* sql`PRAGMA checkpoint_fullfsync = ON;`; yield* runMigrations(); }), );