Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions apps/server/src/persistence/Sqlite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,57 @@ import * as NodePath from "node:path";

import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import * as HostProcess from "@t3tools/shared/HostProcess";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as SqlClient from "effect/sql/SqlClient";

import * as SqlitePersistence from "./Sqlite.ts";

// Linux names an open, already-deleted file "<path> (deleted)" in /proc/self/fd.
// SQLite names its temp files "etilqs_*" and, without TMPDIR, puts them in
// /var/tmp while os.tmpdir() is /tmp, so match the name, not the directory.
const openDeletedTempFiles = () =>
NodeFS.readdirSync("/proc/self/fd").flatMap((fd) => {
try {
const target = NodeFS.readlinkSync(`/proc/self/fd/${fd}`);
return target.includes("/etilqs_") && target.endsWith(" (deleted)") ? [target] : [];
} catch {
return [];
}
});

it.effect.skipIf(HostProcess.Platform.defaultValue() !== "linux")(
"keeps a write transaction's statement journal off the temp directory",
() => {
const tempDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-sqlite-temp-"));
const persistence = SqlitePersistence.layerFromPath(
NodePath.join(tempDir, "state.sqlite"),
).pipe(Layer.provide(NodeServices.layer));

return Effect.gen(function* () {
const sql = yield* SqlClient.SqlClient;
yield* sql`CREATE TABLE journal_probe(id INTEGER PRIMARY KEY, payload TEXT NOT NULL)`;
for (let id = 0; id < 64; id++) {
yield* sql`INSERT INTO journal_probe VALUES (${id}, ${"x".repeat(4096)})`;
}
const before = new Set(openDeletedTempFiles());
const opened = yield* sql.withTransaction(
Effect.gen(function* () {
// A multi-row update can fail halfway, so SQLite journals every page
// it changes, here well past the 64 KiB it holds in memory by default.
yield* sql`UPDATE journal_probe SET payload = payload || 'y'`;
return openDeletedTempFiles().filter((target) => !before.has(target));
}),
);
assert.deepEqual(opened, []);
}).pipe(
Effect.provide(persistence),
Effect.ensuring(Effect.sync(() => NodeFS.rmSync(tempDir, { recursive: true, force: true }))),
);
},
);

it.effect("drops planner statistics a database already has", () => {
const tempDir = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-sqlite-stats-"));
const dbPath = NodePath.join(tempDir, "state.sqlite");
Expand Down
3 changes: 3 additions & 0 deletions apps/server/src/persistence/Sqlite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ const layerSetup = Layer.effectDiscard(
yield* sql`PRAGMA synchronous = NORMAL;`;
// macOS fsync misses drive caches; F_FULLFSYNC flushes them at checkpoints and is ignored elsewhere.
yield* sql`PRAGMA checkpoint_fullfsync = ON;`;
// Keep statement journals and sort spills off the main thread's disk path.
// Large statements and open savepoints can increase peak memory use.
yield* sql`PRAGMA temp_store = MEMORY;`;
// PASSIVE checkpoints retain peak WAL size until the last connection closes.
yield* sql.unsafe(`PRAGMA journal_size_limit = ${WAL_SIZE_LIMIT_BYTES};`);
yield* runMigrations();
Expand Down
Loading