| ... | ... | @@ -13,6 +13,7 @@ import * as Effect from "effect/Effect"; |
| 13 | 13 | import * as Fiber from "effect/Fiber"; |
| 14 | 14 | import { identity } from "effect/Function"; |
| 15 | 15 | import * as Layer from "effect/Layer"; |
| 16 | import * as Schema from "effect/Schema"; |
| 16 | 17 | import * as Scope from "effect/Scope"; |
| 17 | 18 | import * as Semaphore from "effect/Semaphore"; |
| 18 | 19 | import * as Context from "effect/Context"; |
| ... | ... | @@ -45,6 +46,27 @@ export interface SqliteMemoryClientConfig extends Omit< |
| 45 | 46 | "filename" | "readonly" |
| 46 | 47 | > {} |
| 47 | 48 | |
| 49 | export class UnsupportedNodeSqliteVersionError extends Schema.TaggedErrorClass<UnsupportedNodeSqliteVersionError>()( |
| 50 | "UnsupportedNodeSqliteVersionError", |
| 51 | { |
| 52 | nodeVersion: Schema.String, |
| 53 | requirement: Schema.String, |
| 54 | }, |
| 55 | ) { |
| 56 | override get message(): string { |
| 57 | return `Node.js ${this.nodeVersion} is missing required node:sqlite APIs. Upgrade to ${this.requirement}.`; |
| 58 | } |
| 59 | } |
| 60 | |
| 61 | export class UnsupportedNodeSqliteOperationError extends Schema.TaggedErrorClass<UnsupportedNodeSqliteOperationError>()( |
| 62 | "UnsupportedNodeSqliteOperationError", |
| 63 | {}, |
| 64 | ) { |
| 65 | override get message(): string { |
| 66 | return "Node SQLite does not support executeStream."; |
| 67 | } |
| 68 | } |
| 69 | |
| 48 | 70 | /** |
| 49 | 71 | * Verify that the current Node.js version includes the `node:sqlite` APIs |
| 50 | 72 | * used by `NodeSqliteClient` — specifically `StatementSync.columns()` (added |
| ... | ... | @@ -60,8 +82,10 @@ const checkNodeSqliteCompat = () => { |
| 60 | 82 | |
| 61 | 83 | if (!supported) { |
| 62 | 84 | return Effect.die( |
| 63 | | `Node.js ${process.versions.node} is missing required node:sqlite APIs ` + |
| 64 | | `(StatementSync.columns). Upgrade to Node.js >=22.16, >=23.11, or >=24.`, |
| 85 | new UnsupportedNodeSqliteVersionError({ |
| 86 | nodeVersion: process.versions.node, |
| 87 | requirement: "Node.js >=22.16, >=23.11, or >=24", |
| 88 | }), |
| 65 | 89 | ); |
| 66 | 90 | } |
| 67 | 91 | return Effect.void; |
| ... | ... | @@ -70,7 +94,7 @@ const checkNodeSqliteCompat = () => { |
| 70 | 94 | const makeWithDatabase = Effect.fn("makeWithDatabase")(function* ( |
| 71 | 95 | options: SqliteClientConfig, |
| 72 | 96 | openDatabase: () => NodeSqlite.DatabaseSync, |
| 73 | | ): Effect.fn.Return<Client.SqlClient, never, Scope.Scope | Reactivity.Reactivity> { |
| 97 | ): Effect.fn.Return<Client.SqlClient, SqlError, Scope.Scope | Reactivity.Reactivity> { |
| 74 | 98 | yield* checkNodeSqliteCompat(); |
| 75 | 99 | |
| 76 | 100 | const compiler = Statement.makeCompilerSqlite(options.transformQueryNames); |
| ... | ... | @@ -80,10 +104,28 @@ const makeWithDatabase = Effect.fn("makeWithDatabase")(function* ( |
| 80 | 104 | |
| 81 | 105 | const makeConnection = Effect.gen(function* () { |
| 82 | 106 | const scope = yield* Effect.scope; |
| 83 | | const db = openDatabase(); |
| 107 | const db = yield* Effect.try({ |
| 108 | try: openDatabase, |
| 109 | catch: (cause) => |
| 110 | new SqlError({ |
| 111 | reason: classifySqliteError(cause, { |
| 112 | message: "Failed to open database", |
| 113 | operation: "open", |
| 114 | }), |
| 115 | }), |
| 116 | }); |
| 84 | 117 | yield* Scope.addFinalizer( |
| 85 | 118 | scope, |
| 86 | | Effect.sync(() => db.close()), |
| 119 | Effect.try({ |
| 120 | try: () => db.close(), |
| 121 | catch: (cause) => |
| 122 | new SqlError({ |
| 123 | reason: classifySqliteError(cause, { |
| 124 | message: "Failed to close database", |
| 125 | operation: "close", |
| 126 | }), |
| 127 | }), |
| 128 | }).pipe(Effect.orDie), |
| 87 | 129 | ); |
| 88 | 130 | |
| 89 | 131 | const statementReaderCache = new WeakMap<NodeSqlite.StatementSync, boolean>(); |
| ... | ... | @@ -119,8 +161,8 @@ const makeWithDatabase = Effect.fn("makeWithDatabase")(function* ( |
| 119 | 161 | raw: boolean, |
| 120 | 162 | ) => |
| 121 | 163 | Effect.withFiber<ReadonlyArray<any>, SqlError>((fiber) => { |
| 122 | | statement.setReadBigInts(Boolean(Context.get(fiber.context, Client.SafeIntegers))); |
| 123 | 164 | try { |
| 165 | statement.setReadBigInts(Boolean(Context.get(fiber.context, Client.SafeIntegers))); |
| 124 | 166 | if (hasRows(statement)) { |
| 125 | 167 | return Effect.succeed(statement.all(...(params as any))); |
| 126 | 168 | } |
| ... | ... | @@ -166,11 +208,20 @@ const makeWithDatabase = Effect.fn("makeWithDatabase")(function* ( |
| 166 | 208 | }), |
| 167 | 209 | }), |
| 168 | 210 | (statement) => |
| 169 | | Effect.sync(() => { |
| 170 | | if (hasRows(statement)) { |
| 171 | | statement.setReturnArrays(false); |
| 172 | | } |
| 173 | | }), |
| 211 | Effect.try({ |
| 212 | try: () => { |
| 213 | if (hasRows(statement)) { |
| 214 | statement.setReturnArrays(false); |
| 215 | } |
| 216 | }, |
| 217 | catch: (cause) => |
| 218 | new SqlError({ |
| 219 | reason: classifySqliteError(cause, { |
| 220 | message: "Failed to reset statement result mode", |
| 221 | operation: "resetResultMode", |
| 222 | }), |
| 223 | }), |
| 224 | }).pipe(Effect.orDie), |
| 174 | 225 | ); |
| 175 | 226 | |
| 176 | 227 | return identity<Connection>({ |
| ... | ... | @@ -184,11 +235,20 @@ const makeWithDatabase = Effect.fn("makeWithDatabase")(function* ( |
| 184 | 235 | return runValues(sql, params); |
| 185 | 236 | }, |
| 186 | 237 | executeUnprepared(sql, params, rowTransform) { |
| 187 | | const effect = runStatement(db.prepare(sql), params ?? [], false); |
| 238 | const effect = Effect.try({ |
| 239 | try: () => db.prepare(sql), |
| 240 | catch: (cause) => |
| 241 | new SqlError({ |
| 242 | reason: classifySqliteError(cause, { |
| 243 | message: "Failed to prepare statement", |
| 244 | operation: "prepare", |
| 245 | }), |
| 246 | }), |
| 247 | }).pipe(Effect.flatMap((statement) => runStatement(statement, params ?? [], false))); |
| 188 | 248 | return rowTransform ? Effect.map(effect, rowTransform) : effect; |
| 189 | 249 | }, |
| 190 | 250 | executeStream(_sql, _params) { |
| 191 | | return Stream.die("executeStream not implemented"); |
| 251 | return Stream.die(new UnsupportedNodeSqliteOperationError()); |
| 192 | 252 | }, |
| 193 | 253 | }); |
| 194 | 254 | }); |
| ... | ... | @@ -220,7 +280,7 @@ const makeWithDatabase = Effect.fn("makeWithDatabase")(function* ( |
| 220 | 280 | |
| 221 | 281 | const make = ( |
| 222 | 282 | options: SqliteClientConfig, |
| 223 | | ): Effect.Effect<Client.SqlClient, never, Scope.Scope | Reactivity.Reactivity> => |
| 283 | ): Effect.Effect<Client.SqlClient, SqlError, Scope.Scope | Reactivity.Reactivity> => |
| 224 | 284 | makeWithDatabase( |
| 225 | 285 | options, |
| 226 | 286 | () => |
| ... | ... | @@ -232,7 +292,7 @@ const make = ( |
| 232 | 292 | |
| 233 | 293 | const makeMemory = ( |
| 234 | 294 | config: SqliteMemoryClientConfig = {}, |
| 235 | | ): Effect.Effect<Client.SqlClient, never, Scope.Scope | Reactivity.Reactivity> => |
| 295 | ): Effect.Effect<Client.SqlClient, SqlError, Scope.Scope | Reactivity.Reactivity> => |
| 236 | 296 | makeWithDatabase( |
| 237 | 297 | { |
| 238 | 298 | ...config, |
| ... | ... | @@ -249,13 +309,15 @@ const makeMemory = ( |
| 249 | 309 | |
| 250 | 310 | export const layerConfig = ( |
| 251 | 311 | config: Config.Wrap<SqliteClientConfig>, |
| 252 | | ): Layer.Layer<Client.SqlClient, Config.ConfigError> => |
| 312 | ): Layer.Layer<Client.SqlClient, Config.ConfigError | SqlError> => |
| 253 | 313 | Layer.effect(Client.SqlClient, Config.unwrap(config).pipe(Effect.flatMap(make))).pipe( |
| 254 | 314 | Layer.provide(Reactivity.layer), |
| 255 | 315 | ); |
| 256 | 316 | |
| 257 | | export const layer = (config: SqliteClientConfig): Layer.Layer<Client.SqlClient> => |
| 317 | export const layer = (config: SqliteClientConfig): Layer.Layer<Client.SqlClient, SqlError> => |
| 258 | 318 | Layer.effect(Client.SqlClient, make(config)).pipe(Layer.provide(Reactivity.layer)); |
| 259 | 319 | |
| 260 | | export const layerMemory = (config: SqliteMemoryClientConfig = {}): Layer.Layer<Client.SqlClient> => |
| 320 | export const layerMemory = ( |
| 321 | config: SqliteMemoryClientConfig = {}, |
| 322 | ): Layer.Layer<Client.SqlClient, SqlError> => |
| 261 | 323 | Layer.effect(Client.SqlClient, makeMemory(config)).pipe(Layer.provide(Reactivity.layer)); |