| ... | @@ -13,6 +13,7 @@ import * as Exit from "effect/Exit"; | ... | @@ -13,6 +13,7 @@ import * as Exit from "effect/Exit"; |
| 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 Predicate from "effect/Predicate"; |
| 16 | import * as Schema from "effect/Schema"; | 17 | import * as Schema from "effect/Schema"; |
| 17 | import * as Scope from "effect/Scope"; | 18 | import * as Scope from "effect/Scope"; |
| 18 | import * as Semaphore from "effect/Semaphore"; | 19 | import * as Semaphore from "effect/Semaphore"; |
| ... | @@ -82,6 +83,22 @@ const checkNodeSqliteCompat = () => { | ... | @@ -82,6 +83,22 @@ const checkNodeSqliteCompat = () => { |
| 82 | return Effect.void; | 83 | return Effect.void; |
| 83 | }; | 84 | }; |
| 84 | | 85 | |
| | 86 | /** |
| | 87 | * `node:sqlite` reports the SQLite result code as `errcode`, while |
| | 88 | * `classifySqliteError` reads `errno`. Copy it across so busy, locked and |
| | 89 | * constraint failures get their own reasons instead of `UnknownError`. |
| | 90 | */ |
| | 91 | const classifyError = (cause: unknown, message: string, operation: string) => { |
| | 92 | if ( |
| | 93 | Predicate.hasProperty(cause, "errcode") && |
| | 94 | typeof cause.errcode === "number" && |
| | 95 | !Predicate.hasProperty(cause, "errno") |
| | 96 | ) { |
| | 97 | Object.assign(cause, { errno: cause.errcode }); |
| | 98 | } |
| | 99 | return classifySqliteError(cause, { message, operation }); |
| | 100 | }; |
| | 101 | |
| 85 | const make = Effect.fn("makeWithDatabase")(function* ( | 102 | const make = Effect.fn("makeWithDatabase")(function* ( |
| 86 | options: SqliteClientConfig, | 103 | options: SqliteClientConfig, |
| 87 | ): Effect.fn.Return<Client.SqlClient, SqlError, Scope.Scope | Reactivity.Reactivity> { | 104 | ): Effect.fn.Return<Client.SqlClient, SqlError, Scope.Scope | Reactivity.Reactivity> { |
| ... | @@ -102,10 +119,7 @@ const make = Effect.fn("makeWithDatabase")(function* ( | ... | @@ -102,10 +119,7 @@ const make = Effect.fn("makeWithDatabase")(function* ( |
| 102 | }), | 119 | }), |
| 103 | catch: (cause) => | 120 | catch: (cause) => |
| 104 | new SqlError({ | 121 | new SqlError({ |
| 105 | reason: classifySqliteError(cause, { | 122 | reason: classifyError(cause, "Failed to open database", "open"), |
| 106 | message: "Failed to open database", | | |
| 107 | operation: "open", | | |
| 108 | }), | | |
| 109 | }), | 123 | }), |
| 110 | }); | 124 | }); |
| 111 | yield* Scope.addFinalizer( | 125 | yield* Scope.addFinalizer( |
| ... | @@ -114,10 +128,7 @@ const make = Effect.fn("makeWithDatabase")(function* ( | ... | @@ -114,10 +128,7 @@ const make = Effect.fn("makeWithDatabase")(function* ( |
| 114 | try: () => db.close(), | 128 | try: () => db.close(), |
| 115 | catch: (cause) => | 129 | catch: (cause) => |
| 116 | new SqlError({ | 130 | new SqlError({ |
| 117 | reason: classifySqliteError(cause, { | 131 | reason: classifyError(cause, "Failed to close database", "close"), |
| 118 | message: "Failed to close database", | | |
| 119 | operation: "close", | | |
| 120 | }), | | |
| 121 | }), | 132 | }), |
| 122 | }).pipe(Effect.orDie), | 133 | }).pipe(Effect.orDie), |
| 123 | ); | 134 | ); |
| ... | @@ -138,10 +149,7 @@ const make = Effect.fn("makeWithDatabase")(function* ( | ... | @@ -138,10 +149,7 @@ const make = Effect.fn("makeWithDatabase")(function* ( |
| 138 | try: () => db.prepare(sql), | 149 | try: () => db.prepare(sql), |
| 139 | catch: (cause) => | 150 | catch: (cause) => |
| 140 | new SqlError({ | 151 | new SqlError({ |
| 141 | reason: classifySqliteError(cause, { | 152 | reason: classifyError(cause, "Failed to prepare statement", "prepare"), |
| 142 | message: "Failed to prepare statement", | | |
| 143 | operation: "prepare", | | |
| 144 | }), | | |
| 145 | }), | 153 | }), |
| 146 | }); | 154 | }); |
| 147 | | 155 | |
| ... | @@ -168,10 +176,7 @@ const make = Effect.fn("makeWithDatabase")(function* ( | ... | @@ -168,10 +176,7 @@ const make = Effect.fn("makeWithDatabase")(function* ( |
| 168 | } catch (cause) { | 176 | } catch (cause) { |
| 169 | return Effect.fail( | 177 | return Effect.fail( |
| 170 | new SqlError({ | 178 | new SqlError({ |
| 171 | reason: classifySqliteError(cause, { | 179 | reason: classifyError(cause, "Failed to execute statement", "execute"), |
| 172 | message: "Failed to execute statement", | | |
| 173 | operation: "execute", | | |
| 174 | }), | | |
| 175 | }), | 180 | }), |
| 176 | ); | 181 | ); |
| 177 | } | 182 | } |
| ... | @@ -201,10 +206,7 @@ const make = Effect.fn("makeWithDatabase")(function* ( | ... | @@ -201,10 +206,7 @@ const make = Effect.fn("makeWithDatabase")(function* ( |
| 201 | }, | 206 | }, |
| 202 | catch: (cause) => | 207 | catch: (cause) => |
| 203 | new SqlError({ | 208 | new SqlError({ |
| 204 | reason: classifySqliteError(cause, { | 209 | reason: classifyError(cause, "Failed to execute statement", "execute"), |
| 205 | message: "Failed to execute statement", | | |
| 206 | operation: "execute", | | |
| 207 | }), | | |
| 208 | }), | 210 | }), |
| 209 | }), | 211 | }), |
| 210 | (statement) => | 212 | (statement) => |
| ... | @@ -216,10 +218,11 @@ const make = Effect.fn("makeWithDatabase")(function* ( | ... | @@ -216,10 +218,11 @@ const make = Effect.fn("makeWithDatabase")(function* ( |
| 216 | }, | 218 | }, |
| 217 | catch: (cause) => | 219 | catch: (cause) => |
| 218 | new SqlError({ | 220 | new SqlError({ |
| 219 | reason: classifySqliteError(cause, { | 221 | reason: classifyError( |
| 220 | message: "Failed to reset statement result mode", | 222 | cause, |
| 221 | operation: "resetResultMode", | 223 | "Failed to reset statement result mode", |
| 222 | }), | 224 | "resetResultMode", |
| | 225 | ), |
| 223 | }), | 226 | }), |
| 224 | }).pipe(Effect.orDie), | 227 | }).pipe(Effect.orDie), |
| 225 | ); | 228 | ); |
| ... | @@ -273,6 +276,13 @@ const make = Effect.fn("makeWithDatabase")(function* ( | ... | @@ -273,6 +276,13 @@ const make = Effect.fn("makeWithDatabase")(function* ( |
| 273 | acquirer, | 276 | acquirer, |
| 274 | compiler, | 277 | compiler, |
| 275 | transactionAcquirer, | 278 | transactionAcquirer, |
| | 279 | // A deferred BEGIN only takes the write lock at the first write. If another |
| | 280 | // process commits after this transaction's first read, that write fails at |
| | 281 | // once with SQLITE_BUSY_SNAPSHOT, which busy_timeout cannot wait out. Taking |
| | 282 | // the lock up front makes it wait instead, at the cost of serializing |
| | 283 | // read-only transactions behind other processes' writers. Read-only |
| | 284 | // connections cannot write, so they keep the deferred BEGIN. |
| | 285 | beginTransaction: options.readonly === true ? "BEGIN" : "BEGIN IMMEDIATE", |
| 276 | spanAttributes: [ | 286 | spanAttributes: [ |
| 277 | ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []), | 287 | ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []), |
| 278 | [ATTR_DB_SYSTEM_NAME, "sqlite"], | 288 | [ATTR_DB_SYSTEM_NAME, "sqlite"], |