From 687cd659737d91c779889b40f9df818d3c51d77d Mon Sep 17 00:00:00 2001 From: Sebastian Lorenz Date: Tue, 4 Aug 2026 17:56:25 +0000 Subject: [PATCH 1/2] Enforce read-only mode when opening Bun SQLite databases --- packages/sql/sqlite-bun/test/Client.test.ts | 26 +++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/sql/sqlite-bun/test/Client.test.ts b/packages/sql/sqlite-bun/test/Client.test.ts index fc3285a3604..4892f22aa0e 100644 --- a/packages/sql/sqlite-bun/test/Client.test.ts +++ b/packages/sql/sqlite-bun/test/Client.test.ts @@ -1,6 +1,28 @@ -import { describe, it } from "@effect/vitest" +import { Database } from "bun:sqlite" +import { describe, expect, test } from "bun:test" import { Effect } from "effect" +import { Reactivity } from "effect/unstable/reactivity" +import * as SqliteClient from "../src/SqliteClient.ts" describe("Client", () => { - it.effect("should work", () => Effect.void) + test("should work", () => Effect.runPromise(Effect.void)) + + test("readonly clients reject writes", async () => { + const filename = `/tmp/effect-sqlite-bun-readonly-${crypto.randomUUID()}.db` + const db = new Database(filename, { create: true }) + db.run("CREATE TABLE test (id INTEGER PRIMARY KEY)") + db.close() + + try { + const write = Effect.scoped( + Effect.gen(function*() { + const sql = yield* SqliteClient.make({ filename, readonly: true }) + yield* sql`INSERT INTO test DEFAULT VALUES` + }) + ).pipe(Effect.provide(Reactivity.layer), Effect.runPromise) + await expect(write).rejects.toBeDefined() + } finally { + await Bun.file(filename).delete() + } + }) }) From 14ee79b47cf8eb2cf30213a2a984c8a8f901cdd8 Mon Sep 17 00:00:00 2001 From: Tim Smart Date: Tue, 4 Aug 2026 22:10:53 +0000 Subject: [PATCH 2/2] Fix Bun SQLite read-only mode --- .changeset/read-only-bun-sqlite.md | 5 +++ packages/sql/sqlite-bun/src/SqliteClient.ts | 9 +++-- packages/sql/sqlite-bun/test/Client.test.ts | 45 ++++++++++++--------- 3 files changed, 36 insertions(+), 23 deletions(-) create mode 100644 .changeset/read-only-bun-sqlite.md diff --git a/.changeset/read-only-bun-sqlite.md b/.changeset/read-only-bun-sqlite.md new file mode 100644 index 00000000000..9037b67ad05 --- /dev/null +++ b/.changeset/read-only-bun-sqlite.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-sqlite-bun": patch +--- + +Enforce read-only mode when opening Bun SQLite databases. diff --git a/packages/sql/sqlite-bun/src/SqliteClient.ts b/packages/sql/sqlite-bun/src/SqliteClient.ts index 5e7195fb264..745f12a5dab 100644 --- a/packages/sql/sqlite-bun/src/SqliteClient.ts +++ b/packages/sql/sqlite-bun/src/SqliteClient.ts @@ -115,14 +115,15 @@ export const make = ( undefined const makeConnection = Effect.gen(function*() { + const readonly = options.readonly === true const db = new Database(options.filename, { - readonly: options.readonly, - readwrite: options.readwrite ?? true, - create: options.create ?? true + readonly, + readwrite: readonly ? false : options.readwrite ?? true, + create: readonly ? false : options.create ?? true } as any) yield* Effect.addFinalizer(() => Effect.sync(() => db.close())) - if (options.disableWAL !== true) { + if (options.disableWAL !== true && !readonly) { db.run("PRAGMA journal_mode = WAL;") } diff --git a/packages/sql/sqlite-bun/test/Client.test.ts b/packages/sql/sqlite-bun/test/Client.test.ts index 4892f22aa0e..82259957333 100644 --- a/packages/sql/sqlite-bun/test/Client.test.ts +++ b/packages/sql/sqlite-bun/test/Client.test.ts @@ -1,28 +1,35 @@ -import { Database } from "bun:sqlite" -import { describe, expect, test } from "bun:test" +import { assert, describe, it } from "@effect/vitest" import { Effect } from "effect" import { Reactivity } from "effect/unstable/reactivity" -import * as SqliteClient from "../src/SqliteClient.ts" +import { rm } from "node:fs/promises" + +const isBun = "bun" in process.versions describe("Client", () => { - test("should work", () => Effect.runPromise(Effect.void)) + it.effect("should work", () => Effect.void) - test("readonly clients reject writes", async () => { - const filename = `/tmp/effect-sqlite-bun-readonly-${crypto.randomUUID()}.db` - const db = new Database(filename, { create: true }) - db.run("CREATE TABLE test (id INTEGER PRIMARY KEY)") - db.close() + it.effect.skipIf(!isBun)("readonly clients reject writes", () => + Effect.gen(function*() { + const { SqliteClient } = yield* Effect.promise(() => import("@effect/sql-sqlite-bun")) + const filename = `/tmp/effect-sqlite-bun-readonly-${crypto.randomUUID()}.db` + yield* Effect.acquireRelease( + Effect.void, + () => Effect.promise(() => rm(filename, { force: true })) + ) - try { - const write = Effect.scoped( + yield* Effect.scoped( Effect.gen(function*() { - const sql = yield* SqliteClient.make({ filename, readonly: true }) - yield* sql`INSERT INTO test DEFAULT VALUES` + const sql = yield* SqliteClient.make({ filename }) + yield* sql`CREATE TABLE test (id INTEGER PRIMARY KEY)` }) - ).pipe(Effect.provide(Reactivity.layer), Effect.runPromise) - await expect(write).rejects.toBeDefined() - } finally { - await Bun.file(filename).delete() - } - }) + ) + + const sql = yield* SqliteClient.make({ filename, readonly: true }) + assert.deepStrictEqual(yield* sql`SELECT * FROM test`, []) + + const error = yield* Effect.flip(sql`INSERT INTO test DEFAULT VALUES`) + assert.strictEqual(error._tag, "SqlError") + assert(error.reason.cause instanceof Error) + assert.match(error.reason.cause.message, /attempt to write a readonly database/i) + }).pipe(Effect.provide(Reactivity.layer))) })