1import type { Database, SQLQueryBindings, Statement } from "bun:sqlite";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import { SQLite } from "./SQLite.ts";
5import type { SQLiteConnection } from "./SQLiteConnection.ts";
6import { parseError } from "./SQLiteError.ts";
7import type { SQLiteStatement } from "./SQLiteStatement.ts";
8
9/**
10 * Layer that provides the SQLite service using Bun's native SQLite.
11 */
12export const BunSQLite: Layer.Layer<SQLite> = Layer.sync(SQLite, () => ({
13 open: (path: string) =>
14 Effect.gen(function* () {
15 const { Database } = yield* Effect.promise(() => import("bun:sqlite"));
16 const db = new Database(path);
17 // Enable WAL mode for better concurrent read performance
18 db.run("PRAGMA journal_mode = WAL;");
19 // Wait up to 30 seconds when database is locked before returning SQLITE_BUSY
20 db.run("PRAGMA busy_timeout = 30000;");
21 return fromDatabase(db);
22 }),
23}));
24
25/**
26 * Create a SQLiteConnection from a Bun SQLite Database.
27 */
28export const fromDatabase = (db: Database): SQLiteConnection => ({
29 prepare: <R>(sql: string) =>
30 Effect.try({
31 try: () => wrapStatement<R>(db.prepare(sql)),
32 catch: (e) =>
33 parseError(extractErrorCode(e), `Failed to prepare statement: ${e}`, e),
34 }),
35
36 exec: (sql: string) =>
37 Effect.try({
38 try: () => {
39 db.exec(sql);
40 },
41 catch: (e) =>
42 parseError(extractErrorCode(e), `Failed to execute SQL: ${e}`, e),
43 }),
44
45 transaction: <A, E>(
46 fn: (conn: SQLiteConnection) => Effect.Effect<A, E, never>,
47 ) => {
48 // For Bun's synchronous SQLite, we can use the same connection
49 // since everything runs synchronously within the transaction
50 const conn = fromDatabase(db);
51 return Effect.flatMap(
52 Effect.try({
53 try: () => db.transaction(() => Effect.runSync(fn(conn))),
54 catch: (e) =>
55 parseError(
56 extractErrorCode(e),
57 `Failed to create transaction: ${e}`,
58 e,
59 ),
60 }),
61 (txFn) =>
62 Effect.try({
63 try: () => txFn(),
64 catch: (e) =>
65 parseError(extractErrorCode(e), `Transaction failed:`, e),
66 }),
67 );
68 },
69
70 batch: (statements: Array<{ sql: string; params?: unknown[] }>) =>
71 Effect.try({
72 try: () => {
73 // Use Bun's transaction for atomic batch execution
74 db.transaction(() => {
75 for (const stmt of statements) {
76 const prepared = db.prepare(stmt.sql);
77 prepared.run(...((stmt.params ?? []) as SQLQueryBindings[]));
78 }
79 })();
80 },
81 catch: (e) =>
82 parseError(extractErrorCode(e), `Batch execution failed: ${e}`, e),
83 }),
84});
85
86/**
87 * Wrap a Bun SQLite Statement in the SqlStatement interface.
88 */
89const wrapStatement = <R>(stmt: Statement): SQLiteStatement<R> => ({
90 all: <T = R>(...params: unknown[]) =>
91 Effect.try({
92 try: () => stmt.all(...params) as T[],
93 catch: (e) =>
94 parseError(
95 extractErrorCode(e),
96 `Failed to execute statement.all: ${e}`,
97 e,
98 ),
99 }),
100
101 get: <T = R>(...params: unknown[]) =>
102 Effect.try({
103 try: () => stmt.get(...params) as T | undefined,
104 catch: (e) =>
105 parseError(
106 extractErrorCode(e),
107 `Failed to execute statement.get: ${e}`,
108 e,
109 ),
110 }),
111
112 run: (...params: unknown[]) =>
113 Effect.try({
114 try: () => {
115 stmt.run(...params);
116 },
117 catch: (e) =>
118 parseError(
119 extractErrorCode(e),
120 `Failed to execute statement.run: ${e}`,
121 e,
122 ),
123 }),
124});
125
126/**
127 * Extract error code from Bun's SQLiteError.
128 */
129const extractErrorCode = (e: unknown): string | undefined => {
130 const bunError = e as { code?: string };
131 return bunError?.code;
132};