diff --git a/packages/db/src/connection.ts b/packages/db/src/connection.ts index 15a3c04c0c..a4d83d2eb6 100644 --- a/packages/db/src/connection.ts +++ b/packages/db/src/connection.ts @@ -41,6 +41,11 @@ interface TimedStatementOperationArgs { } const DEFAULT_SLOW_DB_QUERY_LOG_THRESHOLD_MS = 100; +/** 256 MiB page cache. Negative cache_size is kibibytes. */ +export const SQLITE_CACHE_SIZE_KIB = 262_144; +/** Memory-map the first 1 GiB of the database file. */ +export const SQLITE_MMAP_SIZE_BYTES = 1_073_741_824; +export const SQLITE_BUSY_TIMEOUT_MS = 5_000; const MAX_LOGGED_SQL_LENGTH = 1_000; const SQL_TRUNCATION_SUFFIX = "..."; // Keep ORM-generated quoted identifiers intact. SQLite accepts double-quoted @@ -171,6 +176,13 @@ export function createConnection( sqlite.pragma("journal_mode = WAL"); // Enable foreign keys sqlite.pragma("foreign_keys = ON"); + // WAL + NORMAL: no fsync per commit. Power loss can drop the last + // transactions; it cannot corrupt the file. cache_size/mmap_size replace + // the 2 MiB default cache so a multi-GB database is not re-read per query. + sqlite.pragma("synchronous = NORMAL"); + sqlite.pragma(`cache_size = -${SQLITE_CACHE_SIZE_KIB}`); + sqlite.pragma(`mmap_size = ${SQLITE_MMAP_SIZE_BYTES}`); + sqlite.pragma(`busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS}`); instrumentSqliteClient(sqlite, options); const db = drizzle({ client: sqlite, schema }); diff --git a/packages/db/test/connection.test.ts b/packages/db/test/connection.test.ts index b70564a1fa..898ab2d250 100644 --- a/packages/db/test/connection.test.ts +++ b/packages/db/test/connection.test.ts @@ -1,7 +1,13 @@ +import { mkdtempSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { describe, expect, it } from "vitest"; import { eq } from "drizzle-orm"; import { createConnection, + SQLITE_BUSY_TIMEOUT_MS, + SQLITE_CACHE_SIZE_KIB, + SQLITE_MMAP_SIZE_BYTES, type SlowDbQueryLogger, type SlowDbQueryLogFields, } from "../src/connection.js"; @@ -124,4 +130,36 @@ describe("createConnection", () => { db.$client.close(); }); + + it("applies the hot-path sqlite pragmas on a file database", () => { + const directory = mkdtempSync(join(tmpdir(), "bb-db-pragmas-")); + const db = createConnection(join(directory, "bb.db")); + + try { + expect( + db.$client.prepare("PRAGMA cache_size").get() as { + cache_size: number; + }, + ).toEqual({ cache_size: -SQLITE_CACHE_SIZE_KIB }); + expect( + db.$client.prepare("PRAGMA synchronous").get() as { + synchronous: number; + }, + ).toEqual({ synchronous: 1 }); + expect( + db.$client.prepare("PRAGMA mmap_size").get() as { mmap_size: number }, + ).toEqual({ mmap_size: SQLITE_MMAP_SIZE_BYTES }); + expect( + db.$client.prepare("PRAGMA busy_timeout").get() as { timeout: number }, + ).toEqual({ timeout: SQLITE_BUSY_TIMEOUT_MS }); + expect( + db.$client.prepare("PRAGMA journal_mode").get() as { + journal_mode: string; + }, + ).toEqual({ journal_mode: "wal" }); + } finally { + db.$client.close(); + rmSync(directory, { force: true, recursive: true }); + } + }); });