| 1 | import type { Database, SQLQueryBindings, Statement } from "bun:sqlite"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Layer from "effect/Layer"; |
| 4 | import { SQLite } from "./SQLite.ts"; |
| 5 | import type { SQLiteConnection } from "./SQLiteConnection.ts"; |
| 6 | import { parseError } from "./SQLiteError.ts"; |
| 7 | import type { SQLiteStatement } from "./SQLiteStatement.ts"; |
| 8 | |
| 9 | /** |
| 10 | * Layer that provides the SQLite service using Bun's native SQLite. |
| 11 | */ |
| 12 | export 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 | */ |
| 28 | export 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 | */ |
| 89 | const 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 | */ |
| 129 | const extractErrorCode = (e: unknown): string | undefined => { |
| 130 | const bunError = e as { code?: string }; |
| 131 | return bunError?.code; |
| 132 | }; |