| ... | @@ -13,6 +13,7 @@ import * as Effect from "effect/Effect"; | ... | @@ -13,6 +13,7 @@ import * as Effect from "effect/Effect"; |
| 13 | import * as Fiber from "effect/Fiber"; | 13 | import * as Fiber from "effect/Fiber"; |
| 14 | import { identity } from "effect/Function"; | 14 | import { identity } from "effect/Function"; |
| 15 | import * as Layer from "effect/Layer"; | 15 | import * as Layer from "effect/Layer"; |
| | 16 | import * as Schema from "effect/Schema"; |
| 16 | import * as Scope from "effect/Scope"; | 17 | import * as Scope from "effect/Scope"; |
| 17 | import * as Semaphore from "effect/Semaphore"; | 18 | import * as Semaphore from "effect/Semaphore"; |
| 18 | import * as Context from "effect/Context"; | 19 | import * as Context from "effect/Context"; |
| ... | @@ -45,6 +46,27 @@ export interface SqliteMemoryClientConfig extends Omit< | ... | @@ -45,6 +46,27 @@ export interface SqliteMemoryClientConfig extends Omit< |
| 45 | "filename" | "readonly" | 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 | * Verify that the current Node.js version includes the `node:sqlite` APIs | 71 | * Verify that the current Node.js version includes the `node:sqlite` APIs |
| 50 | * used by `NodeSqliteClient` — specifically `StatementSync.columns()` (added | 72 | * used by `NodeSqliteClient` — specifically `StatementSync.columns()` (added |
| ... | @@ -60,8 +82,10 @@ const checkNodeSqliteCompat = () => { | ... | @@ -60,8 +82,10 @@ const checkNodeSqliteCompat = () => { |
| 60 | | 82 | |
| 61 | if (!supported) { | 83 | if (!supported) { |
| 62 | return Effect.die( | 84 | return Effect.die( |
| 63 | `Node.js ${process.versions.node} is missing required node:sqlite APIs ` + | 85 | new UnsupportedNodeSqliteVersionError({ |
| 64 | `(StatementSync.columns). Upgrade to Node.js >=22.16, >=23.11, or >=24.`, | 86 | nodeVersion: process.versions.node, |
| | 87 | requirement: "Node.js >=22.16, >=23.11, or >=24", |
| | 88 | }), |
| 65 | ); | 89 | ); |
| 66 | } | 90 | } |
| 67 | return Effect.void; | 91 | return Effect.void; |
| ... | @@ -70,7 +94,7 @@ const checkNodeSqliteCompat = () => { | ... | @@ -70,7 +94,7 @@ const checkNodeSqliteCompat = () => { |
| 70 | const makeWithDatabase = Effect.fn("makeWithDatabase")(function* ( | 94 | const makeWithDatabase = Effect.fn("makeWithDatabase")(function* ( |
| 71 | options: SqliteClientConfig, | 95 | options: SqliteClientConfig, |
| 72 | openDatabase: () => NodeSqlite.DatabaseSync, | 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 | yield* checkNodeSqliteCompat(); | 98 | yield* checkNodeSqliteCompat(); |
| 75 | | 99 | |
| 76 | const compiler = Statement.makeCompilerSqlite(options.transformQueryNames); | 100 | const compiler = Statement.makeCompilerSqlite(options.transformQueryNames); |
| ... | @@ -80,10 +104,28 @@ const makeWithDatabase = Effect.fn("makeWithDatabase")(function* ( | ... | @@ -80,10 +104,28 @@ const makeWithDatabase = Effect.fn("makeWithDatabase")(function* ( |
| 80 | | 104 | |
| 81 | const makeConnection = Effect.gen(function* () { | 105 | const makeConnection = Effect.gen(function* () { |
| 82 | const scope = yield* Effect.scope; | 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 | yield* Scope.addFinalizer( | 117 | yield* Scope.addFinalizer( |
| 85 | scope, | 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 | const statementReaderCache = new WeakMap<NodeSqlite.StatementSync, boolean>(); | 131 | const statementReaderCache = new WeakMap<NodeSqlite.StatementSync, boolean>(); |
| ... | @@ -119,8 +161,8 @@ const makeWithDatabase = Effect.fn("makeWithDatabase")(function* ( | ... | @@ -119,8 +161,8 @@ const makeWithDatabase = Effect.fn("makeWithDatabase")(function* ( |
| 119 | raw: boolean, | 161 | raw: boolean, |
| 120 | ) => | 162 | ) => |
| 121 | Effect.withFiber<ReadonlyArray<any>, SqlError>((fiber) => { | 163 | Effect.withFiber<ReadonlyArray<any>, SqlError>((fiber) => { |
| 122 | statement.setReadBigInts(Boolean(Context.get(fiber.context, Client.SafeIntegers))); | | |
| 123 | try { | 164 | try { |
| | 165 | statement.setReadBigInts(Boolean(Context.get(fiber.context, Client.SafeIntegers))); |
| 124 | if (hasRows(statement)) { | 166 | if (hasRows(statement)) { |
| 125 | return Effect.succeed(statement.all(...(params as any))); | 167 | return Effect.succeed(statement.all(...(params as any))); |
| 126 | } | 168 | } |
| ... | @@ -166,11 +208,20 @@ const makeWithDatabase = Effect.fn("makeWithDatabase")(function* ( | ... | @@ -166,11 +208,20 @@ const makeWithDatabase = Effect.fn("makeWithDatabase")(function* ( |
| 166 | }), | 208 | }), |
| 167 | }), | 209 | }), |
| 168 | (statement) => | 210 | (statement) => |
| 169 | Effect.sync(() => { | 211 | Effect.try({ |
| 170 | if (hasRows(statement)) { | 212 | try: () => { |
| 171 | statement.setReturnArrays(false); | 213 | if (hasRows(statement)) { |
| 172 | } | 214 | statement.setReturnArrays(false); |
| 173 | }), | 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 | return identity<Connection>({ | 227 | return identity<Connection>({ |
| ... | @@ -184,11 +235,20 @@ const makeWithDatabase = Effect.fn("makeWithDatabase")(function* ( | ... | @@ -184,11 +235,20 @@ const makeWithDatabase = Effect.fn("makeWithDatabase")(function* ( |
| 184 | return runValues(sql, params); | 235 | return runValues(sql, params); |
| 185 | }, | 236 | }, |
| 186 | executeUnprepared(sql, params, rowTransform) { | 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 | return rowTransform ? Effect.map(effect, rowTransform) : effect; | 248 | return rowTransform ? Effect.map(effect, rowTransform) : effect; |
| 189 | }, | 249 | }, |
| 190 | executeStream(_sql, _params) { | 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,7 +280,7 @@ const makeWithDatabase = Effect.fn("makeWithDatabase")(function* ( |
| 220 | | 280 | |
| 221 | const make = ( | 281 | const make = ( |
| 222 | options: SqliteClientConfig, | 282 | options: SqliteClientConfig, |
| 223 | ): Effect.Effect<Client.SqlClient, never, Scope.Scope | Reactivity.Reactivity> => | 283 | ): Effect.Effect<Client.SqlClient, SqlError, Scope.Scope | Reactivity.Reactivity> => |
| 224 | makeWithDatabase( | 284 | makeWithDatabase( |
| 225 | options, | 285 | options, |
| 226 | () => | 286 | () => |
| ... | @@ -232,7 +292,7 @@ const make = ( | ... | @@ -232,7 +292,7 @@ const make = ( |
| 232 | | 292 | |
| 233 | const makeMemory = ( | 293 | const makeMemory = ( |
| 234 | config: SqliteMemoryClientConfig = {}, | 294 | config: SqliteMemoryClientConfig = {}, |
| 235 | ): Effect.Effect<Client.SqlClient, never, Scope.Scope | Reactivity.Reactivity> => | 295 | ): Effect.Effect<Client.SqlClient, SqlError, Scope.Scope | Reactivity.Reactivity> => |
| 236 | makeWithDatabase( | 296 | makeWithDatabase( |
| 237 | { | 297 | { |
| 238 | ...config, | 298 | ...config, |
| ... | @@ -249,13 +309,15 @@ const makeMemory = ( | ... | @@ -249,13 +309,15 @@ const makeMemory = ( |
| 249 | | 309 | |
| 250 | export const layerConfig = ( | 310 | export const layerConfig = ( |
| 251 | config: Config.Wrap<SqliteClientConfig>, | 311 | config: Config.Wrap<SqliteClientConfig>, |
| 252 | ): Layer.Layer<Client.SqlClient, Config.ConfigError> => | 312 | ): Layer.Layer<Client.SqlClient, Config.ConfigError | SqlError> => |
| 253 | Layer.effect(Client.SqlClient, Config.unwrap(config).pipe(Effect.flatMap(make))).pipe( | 313 | Layer.effect(Client.SqlClient, Config.unwrap(config).pipe(Effect.flatMap(make))).pipe( |
| 254 | Layer.provide(Reactivity.layer), | 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 | Layer.effect(Client.SqlClient, make(config)).pipe(Layer.provide(Reactivity.layer)); | 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 | Layer.effect(Client.SqlClient, makeMemory(config)).pipe(Layer.provide(Reactivity.layer)); | 323 | Layer.effect(Client.SqlClient, makeMemory(config)).pipe(Layer.provide(Reactivity.layer)); |