import { Database } from "bun:sqlite" import { drizzle as drizzleBun, type SQLiteBunDatabase } from "drizzle-orm/bun-sqlite" import type { AnyRelations, EmptyRelations } from "drizzle-orm/relations" import { SQLiteCountBuilder } from "drizzle-orm/sqlite-core/query-builders/count" import { SQLiteDeleteBase } from "drizzle-orm/sqlite-core/query-builders/delete" import { SQLiteInsertBase } from "drizzle-orm/sqlite-core/query-builders/insert" import { SQLiteRelationalQuery, SQLiteSyncRelationalQuery } from "drizzle-orm/sqlite-core/query-builders/_query" import { SQLiteSelectBase } from "drizzle-orm/sqlite-core/query-builders/select" import { SQLiteUpdateBase } from "drizzle-orm/sqlite-core/query-builders/update" import type { PreparedQueryConfig, SQLiteSession, SQLiteTransaction, SQLiteTransactionConfig } from "drizzle-orm/sqlite-core/session" import { SQLitePreparedQuery } from "drizzle-orm/sqlite-core/session" import type { DrizzleConfig } from "drizzle-orm/utils" import { Cause, Effect, Exit, Schema } from "effect" import { pipeArguments } from "effect/Pipeable" export class EffectDrizzleQueryError extends Schema.TaggedErrorClass()( "EffectDrizzleQueryError", { query: Schema.String, params: Schema.Array(Schema.Unknown), cause: Schema.Unknown, }, ) { override get message() { return `Failed query: ${this.query}\nparams: ${JSON.stringify(this.params)}` } } export type EffectSQLiteDatabase< TSchema extends Record = Record, TRelations extends AnyRelations = EmptyRelations, > = SQLiteBunDatabase & { readonly $client: Database readonly withTransaction: ( effect: Effect.Effect, config?: SQLiteTransactionConfig, ) => Effect.Effect } export type MakeConfig< TSchema extends Record = Record, TRelations extends AnyRelations = EmptyRelations, > = DrizzleConfig & { readonly client?: Database readonly filename?: string } type EffectLikeQuery = { readonly asEffect?: () => Effect.Effect readonly toSQL?: () => { readonly sql: string; readonly params?: readonly unknown[] } } type PreparedLike = EffectLikeQuery & { readonly execute: () => unknown readonly getQuery?: () => { readonly sql: string; readonly params?: readonly unknown[] } } type SelectLike = EffectLikeQuery & { readonly all: () => A } type MutationLike = EffectLikeQuery & { readonly all: () => A readonly run: () => A readonly config?: { readonly returning?: unknown } } type CountLike = EffectLikeQuery & { readonly session: { readonly values: (sql: unknown) => unknown[][] } readonly sql: unknown } class TransactionFailure extends Error { constructor(readonly effectCause: Cause.Cause) { super("Effect transaction failed") } } const EffectTypeId = "~effect/Effect" const EffectIdentifier = `${EffectTypeId}/identifier` const EffectEvaluate = `${EffectTypeId}/evaluate` const effectVariance = { _A: (value: unknown) => value, _E: (value: unknown) => value, _R: (value: unknown) => value, } const queryInfo = (query: EffectLikeQuery | PreparedLike) => { const info = "getQuery" in query && typeof query.getQuery === "function" ? query.getQuery() : query.toSQL?.() return { query: info?.sql ?? "", params: [...(info?.params ?? [])], } } const queryError = (query: EffectLikeQuery | PreparedLike, cause: unknown) => new EffectDrizzleQueryError({ ...queryInfo(query), cause, }) const fromSync = (query: EffectLikeQuery, run: () => A) => Effect.try({ try: run, catch: (cause) => queryError(query, cause), }) const fromMutation = (query: MutationLike) => fromSync(query, () => (query.config?.returning ? query.all() : query.run())) const fromCount = (query: CountLike) => fromSync(query, () => Number(query.session.values(query.sql)[0]?.[0] ?? 0)) const fromExecuteResult = (result: unknown) => { if (result && typeof result === "object" && "sync" in result && typeof result.sync === "function") { return result.sync() } return result } const queryEffectProto = { [EffectTypeId]: effectVariance, pipe() { return pipeArguments(this, arguments) }, [Symbol.iterator]() { let done = false const self = this return { next(value: unknown) { if (done) return { done: true, value } done = true return { done: false, value: self } }, [Symbol.iterator]() { return this }, } }, [EffectIdentifier]: "DrizzleSqliteQuery", [EffectEvaluate](this: EffectLikeQuery) { return this.asEffect?.() ?? Effect.die("Drizzle SQLite query is missing asEffect()") }, } const patchClass = (ctor: { readonly prototype: object }, asEffect: (self: A) => Effect.Effect) => { if (Object.prototype.hasOwnProperty.call(ctor.prototype, "asEffect")) return Object.assign(ctor.prototype, queryEffectProto, { asEffect(this: A) { return asEffect(this) }, }) } const patchQueryBuilders = (() => { let patched = false return () => { if (patched) return patched = true patchClass(SQLitePreparedQuery, (query: PreparedLike) => fromSync(query, () => fromExecuteResult(query.execute()))) patchClass(SQLiteSelectBase, (query: SelectLike) => fromSync(query, () => query.all())) patchClass(SQLiteInsertBase, fromMutation) patchClass(SQLiteUpdateBase, fromMutation) patchClass(SQLiteDeleteBase, fromMutation) patchClass(SQLiteRelationalQuery, (query: EffectLikeQuery & { readonly executeRaw: () => unknown }) => fromSync(query, () => query.executeRaw()), ) patchClass(SQLiteSyncRelationalQuery, (query: EffectLikeQuery & { readonly executeRaw: () => unknown }) => fromSync(query, () => query.executeRaw()), ) patchClass(SQLiteCountBuilder, fromCount) } })() const attachTransaction = < TSchema extends Record = Record, TRelations extends AnyRelations = EmptyRelations, >(db: SQLiteBunDatabase & { readonly $client: Database }): EffectSQLiteDatabase => { const txStack: Array> = [] const current = () => txStack.at(-1) ?? db const runTransaction = (target: SQLiteBunDatabase | SQLiteTransaction<"sync", void, TSchema, TRelations>) => target.transaction.bind(target) as ( transaction: (tx: SQLiteTransaction<"sync", void, TSchema, TRelations>) => unknown, config?: SQLiteTransactionConfig, ) => unknown const withTransaction = ( effect: Effect.Effect, config?: SQLiteTransactionConfig, ): Effect.Effect => Effect.context().pipe( Effect.flatMap((context) => Effect.sync( () => runTransaction(current())((tx) => { txStack.push(tx) try { const exit = Effect.runSyncExit(Effect.provideContext(effect, context)) if (Exit.isSuccess(exit)) return exit.value throw new TransactionFailure(exit.cause) } finally { txStack.pop() } }, config) as A, ).pipe( Effect.catchDefect((defect) => defect instanceof TransactionFailure ? Effect.failCause(defect.effectCause as Cause.Cause) : Effect.die(defect), ), ), ), ) return new Proxy(db, { get(_target, property) { if (property === "withTransaction") return withTransaction if (property === "$client") return db.$client const target = current() const value = Reflect.get(target, property) return typeof value === "function" ? value.bind(target) : value }, }) as EffectSQLiteDatabase } export const make = < TSchema extends Record = Record, TRelations extends AnyRelations = EmptyRelations, >(config: MakeConfig = {}): EffectSQLiteDatabase => { patchQueryBuilders() const { client, filename, ...drizzleConfig } = config return attachTransaction( drizzleBun({ ...drizzleConfig, client: client ?? new Database(filename ?? ":memory:"), }), ) } export const drizzle = make declare module "drizzle-orm/query-promise" { interface QueryPromise extends Effect.Effect { asEffect(): Effect.Effect } } declare module "drizzle-orm/sqlite-core/session" { interface SQLitePreparedQuery extends Effect.Effect { asEffect(): Effect.Effect } } declare module "drizzle-orm/sqlite-core/query-builders/count" { interface SQLiteCountBuilder> extends Effect.Effect { asEffect(): Effect.Effect } }