authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-10-14 01:18:11-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-14 01:18:11-07:00
log614b432a79b0acae38c27c00b1e4eece223ecfe4
treeec5bd1cac63cd222d49384ddadf3179b45cd6275
parentacb90279e2adad19a63586944543fe7346499467
signature Signed by PGP key B5690EEEBB952194

fix(server): make SQLite checkpoints durable on macOS (#19304)

Co-authored-by: Soroush Baraouf <soroushraouf44@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

4 files changed, 36 insertions(+), 0 deletions(-)

apps/server/scripts/t3-sqlite-state.test.ts+19
......@@ -75,6 +75,25 @@ it.layer(NodeServices.layer)("t3-sqlite-state", (it) => {
7575 }),
7676 );
7777
78 it.effect("enables checkpoint_fullfsync on its direct connection", () =>
79 Effect.gen(function* () {
80 const fs = yield* FileSystem.FileSystem;
81 const baseDir = yield* fs.makeTempDirectoryScoped({ prefix: "t3-sqlite-state-fsync-" });
82 yield* createFixtureDatabase(baseDir);
83
84 const result = yield* runSqliteState({
85 operation: "query",
86 baseDir,
87 sql: "PRAGMA checkpoint_fullfsync",
88 });
89
90 assert.equal(result.operation, "query");
91 if (result.operation === "query") {
92 assert.deepStrictEqual(result.rows, [{ checkpoint_fullfsync: 1 }]);
93 }
94 }),
95 );
96
7897 it.effect.skipIf(!symlinksSupported)(
7998 "backs up isolated state before writes and refuses the shared home",
8099 () =>
apps/server/scripts/t3-sqlite-state.ts+3
......@@ -201,6 +201,9 @@ export const runSqliteState = Effect.fn("runSqliteState")(function* (
201201 const program = Effect.gen(function* () {
202202 const sql = yield* SqlClient.SqlClient;
203203 yield* sql.unsafe("PRAGMA busy_timeout = 5000").unprepared;
204 // This connection bypasses persistence/Sqlite.ts and can checkpoint
205 // statev2.sqlite, so it needs the same F_FULLFSYNC checkpoint barrier.
206 yield* sql.unsafe("PRAGMA checkpoint_fullfsync = ON").unprepared;
204207
205208 if (input.operation === "query") {
206209 const rows = yield* sql.unsafe<RawSqliteRow>(source).unprepared.pipe(
apps/server/src/persistence/Sqlite.test.ts+10
......@@ -136,3 +136,13 @@ it.effect("applies busy_timeout in the shared persistence setup", () =>
136136 assert.equal(rows[0]?.timeout, 5000);
137137 }).pipe(Effect.provide(SqlitePersistence.layerMemory)),
138138);
139
140it.effect("enables checkpoint_fullfsync in the shared persistence setup", () =>
141 Effect.gen(function* () {
142 const sql = yield* SqlClient.SqlClient;
143 const rows = yield* sql<{
144 readonly checkpoint_fullfsync: number;
145 }>`PRAGMA checkpoint_fullfsync`;
146 assert.equal(rows[0]?.checkpoint_fullfsync, 1);
147 }).pipe(Effect.provide(SqlitePersistence.layerMemory)),
148);
apps/server/src/persistence/Sqlite.ts+4
......@@ -25,6 +25,10 @@ const layerSetup = Layer.effectDiscard(
2525 // cannot corrupt the database and an app crash loses nothing; an OS crash
2626 // or power loss can roll back commits since the last checkpoint.
2727 yield* sql`PRAGMA synchronous = NORMAL;`;
28 // Checkpoints are now the only sync, and macOS fsync() does not flush the
29 // drive cache. F_FULLFSYNC makes them reach disk; other platforms ignore it.
30 // Per-commit fullfsync stays off.
31 yield* sql`PRAGMA checkpoint_fullfsync = ON;`;
2832 // PASSIVE checkpoints never shrink the -wal file, so it otherwise keeps its
2933 // largest size until the last connection closes.
3034 yield* sql.unsafe(`PRAGMA journal_size_limit = ${WAL_SIZE_LIMIT_BYTES};`);