/** * Connects Effect SQL to SQLite when running on Bun, using `bun:sqlite`. * * This module opens a SQLite database and exposes it as both `SqliteClient` and * the generic Effect SQL client. It serializes access to the database, enables * WAL mode unless disabled, and supports database export and extension loading. * Streaming queries and `updateValues` are not supported by this driver. * * @since 4.0.0 */ import { Database } from "bun:sqlite" import * as Config from "effect/Config" import * as Context from "effect/Context" import * as Effect from "effect/Effect" import * as Fiber from "effect/Fiber" import { identity } from "effect/Function" import * as Layer from "effect/Layer" import * as Scope from "effect/Scope" import * as Semaphore from "effect/Semaphore" import * as Stream from "effect/Stream" import * as Reactivity from "effect/unstable/reactivity/Reactivity" import * as Client from "effect/unstable/sql/SqlClient" import type { Connection } from "effect/unstable/sql/SqlConnection" import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError" import * as Statement from "effect/unstable/sql/Statement" const ATTR_DB_SYSTEM_NAME = "db.system.name" const classifyError = (cause: unknown, message: string, operation: string) => classifySqliteError(cause, { message, operation }) /** * Runtime type identifier used to mark Bun `SqliteClient` values. * * @category type IDs * @since 4.0.0 */ export const TypeId: TypeId = "~@effect/sql-sqlite-bun/SqliteClient" /** * Type-level identifier used to mark Bun `SqliteClient` values. * * @category type IDs * @since 4.0.0 */ export type TypeId = "~@effect/sql-sqlite-bun/SqliteClient" /** * Bun SQLite client service, extending `SqlClient` with database export and extension loading helpers. `updateValues` is not supported. * * @category models * @since 4.0.0 */ export interface SqliteClient extends Client.SqlClient { readonly [TypeId]: TypeId readonly config: SqliteClientConfig readonly export: Effect.Effect readonly loadExtension: (path: string) => Effect.Effect /** Not supported in sqlite */ readonly updateValues: never } /** * Service tag for the Bun SQLite client service. * * **When to use** * * Use to access or provide a Bun SQLite client through the Effect context. * * @category services * @since 4.0.0 */ export const SqliteClient = Context.Service("@effect/sql-sqlite-bun/Client") /** * Configuration for a Bun SQLite client, including filename, open mode flags, WAL behavior, span attributes, and query/result name transforms. * * @category models * @since 4.0.0 */ export interface SqliteClientConfig { readonly filename: string readonly readonly?: boolean | undefined readonly create?: boolean | undefined readonly readwrite?: boolean | undefined readonly disableWAL?: boolean | undefined readonly spanAttributes?: Record | undefined readonly transformResultNames?: ((str: string) => string) | undefined readonly transformQueryNames?: ((str: string) => string) | undefined } interface SqliteConnection extends Connection { readonly export: Effect.Effect readonly loadExtension: (path: string) => Effect.Effect } /** * Creates a scoped Bun SQLite client for a database file, enabling WAL by default and serializing access. Streaming queries are not implemented. * * @category constructors * @since 4.0.0 */ export const make = ( options: SqliteClientConfig ): Effect.Effect => Effect.gen(function*() { const compiler = Statement.makeCompilerSqlite(options.transformQueryNames) const transformRows = options.transformResultNames ? Statement.defaultTransforms( options.transformResultNames ).array : undefined const makeConnection = Effect.gen(function*() { const db = new Database(options.filename, { readonly: options.readonly, readwrite: options.readwrite ?? true, create: options.create ?? true } as any) yield* Effect.addFinalizer(() => Effect.sync(() => db.close())) if (options.disableWAL !== true) { db.run("PRAGMA journal_mode = WAL;") } const prepare = (sql: string, useSafeIntegers: boolean) => { const statement = db.query(sql) // @ts-ignore bun-types missing safeIntegers method, fixed in https://github.com/oven-sh/bun/pull/26627 statement.safeIntegers(useSafeIntegers) return statement } const run = ( sql: string, params: ReadonlyArray = [] ) => Effect.withFiber, SqlError>((fiber) => { const useSafeIntegers = Context.get(fiber.context, Client.SafeIntegers) try { return Effect.succeed((prepare(sql, useSafeIntegers).all(...(params as any)) ?? []) as Array) } catch (cause) { return Effect.fail(new SqlError({ reason: classifyError(cause, "Failed to execute statement", "execute") })) } }) const runValues = ( sql: string, params: ReadonlyArray = [] ) => Effect.withFiber, SqlError>((fiber) => { const useSafeIntegers = Context.get(fiber.context, Client.SafeIntegers) try { return Effect.succeed((prepare(sql, useSafeIntegers).values(...(params as any)) ?? []) as Array) } catch (cause) { return Effect.fail( new SqlError({ reason: classifyError(cause, "Failed to execute statement", "executeValues") }) ) } }) return identity({ execute(sql, params, transformRows) { return transformRows ? Effect.map(run(sql, params), transformRows) : run(sql, params) }, executeRaw(sql, params) { return run(sql, params) }, executeValues(sql, params) { return runValues(sql, params) }, executeValuesUnprepared(sql, params) { return runValues(sql, params) }, executeUnprepared(sql, params, transformRows) { return this.execute(sql, params, transformRows) }, executeStream(_sql, _params) { return Stream.die("executeStream not implemented") }, export: Effect.try({ try: () => db.serialize(), catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to export database", "export") }) }), loadExtension: (path) => Effect.try({ try: () => db.loadExtension(path), catch: (cause) => new SqlError({ reason: classifyError(cause, "Failed to load extension", "loadExtension") }) }) }) }) const semaphore = yield* Semaphore.make(1) const connection = yield* makeConnection const acquirer = semaphore.withPermits(1)(Effect.succeed(connection)) const transactionAcquirer = Effect.uninterruptibleMask((restore) => { const fiber = Fiber.getCurrent()! const scope = Context.getUnsafe(fiber.context, Scope.Scope) return Effect.as( Effect.tap( restore(semaphore.take(1)), () => Scope.addFinalizer(scope, semaphore.release(1)) ), connection ) }) return Object.assign( (yield* Client.make({ acquirer, compiler, transactionAcquirer, spanAttributes: [ ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []), [ATTR_DB_SYSTEM_NAME, "sqlite"] ], transformRows })) as SqliteClient, { [TypeId]: TypeId as TypeId, config: options, export: Effect.flatMap(acquirer, (_) => _.export), loadExtension: (path: string) => Effect.flatMap(acquirer, (_) => _.loadExtension(path)) } ) }) /** * Creates a layer from a `Config`-wrapped Bun SQLite client configuration, providing both `SqliteClient` and `SqlClient`. * * @category layers * @since 4.0.0 */ export const layerConfig = ( config: Config.Wrap ): Layer.Layer => Layer.effectContext( Config.unwrap(config).pipe( Effect.flatMap(make), Effect.map((client) => Context.make(SqliteClient, client).pipe( Context.add(Client.SqlClient, client) ) ) ) ).pipe(Layer.provide(Reactivity.layer)) /** * Creates a layer from a concrete Bun SQLite client configuration, providing both `SqliteClient` and `SqlClient`. * * @category layers * @since 4.0.0 */ export const layer = ( config: SqliteClientConfig ): Layer.Layer => Layer.effectContext( Effect.map(make(config), (client) => Context.make(SqliteClient, client).pipe( Context.add(Client.SqlClient, client) )) ).pipe(Layer.provide(Reactivity.layer))