| ... | ... | @@ -13,6 +13,7 @@ import * as Exit from "effect/Exit"; |
| 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 Predicate from "effect/Predicate"; |
| 16 | 17 | import * as Schema from "effect/Schema"; |
| 17 | 18 | import * as Scope from "effect/Scope"; |
| 18 | 19 | import * as Semaphore from "effect/Semaphore"; |
| ... | ... | @@ -82,6 +83,22 @@ const checkNodeSqliteCompat = () => { |
| 82 | 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 | 102 | const make = Effect.fn("makeWithDatabase")(function* ( |
| 86 | 103 | options: SqliteClientConfig, |
| 87 | 104 | ): Effect.fn.Return<Client.SqlClient, SqlError, Scope.Scope | Reactivity.Reactivity> { |
| ... | ... | @@ -102,10 +119,7 @@ const make = Effect.fn("makeWithDatabase")(function* ( |
| 102 | 119 | }), |
| 103 | 120 | catch: (cause) => |
| 104 | 121 | new SqlError({ |
| 105 | | reason: classifySqliteError(cause, { |
| 106 | | message: "Failed to open database", |
| 107 | | operation: "open", |
| 108 | | }), |
| 122 | reason: classifyError(cause, "Failed to open database", "open"), |
| 109 | 123 | }), |
| 110 | 124 | }); |
| 111 | 125 | yield* Scope.addFinalizer( |
| ... | ... | @@ -114,10 +128,7 @@ const make = Effect.fn("makeWithDatabase")(function* ( |
| 114 | 128 | try: () => db.close(), |
| 115 | 129 | catch: (cause) => |
| 116 | 130 | new SqlError({ |
| 117 | | reason: classifySqliteError(cause, { |
| 118 | | message: "Failed to close database", |
| 119 | | operation: "close", |
| 120 | | }), |
| 131 | reason: classifyError(cause, "Failed to close database", "close"), |
| 121 | 132 | }), |
| 122 | 133 | }).pipe(Effect.orDie), |
| 123 | 134 | ); |
| ... | ... | @@ -138,10 +149,7 @@ const make = Effect.fn("makeWithDatabase")(function* ( |
| 138 | 149 | try: () => db.prepare(sql), |
| 139 | 150 | catch: (cause) => |
| 140 | 151 | new SqlError({ |
| 141 | | reason: classifySqliteError(cause, { |
| 142 | | message: "Failed to prepare statement", |
| 143 | | operation: "prepare", |
| 144 | | }), |
| 152 | reason: classifyError(cause, "Failed to prepare statement", "prepare"), |
| 145 | 153 | }), |
| 146 | 154 | }); |
| 147 | 155 | |
| ... | ... | @@ -168,10 +176,7 @@ const make = Effect.fn("makeWithDatabase")(function* ( |
| 168 | 176 | } catch (cause) { |
| 169 | 177 | return Effect.fail( |
| 170 | 178 | new SqlError({ |
| 171 | | reason: classifySqliteError(cause, { |
| 172 | | message: "Failed to execute statement", |
| 173 | | operation: "execute", |
| 174 | | }), |
| 179 | reason: classifyError(cause, "Failed to execute statement", "execute"), |
| 175 | 180 | }), |
| 176 | 181 | ); |
| 177 | 182 | } |
| ... | ... | @@ -201,10 +206,7 @@ const make = Effect.fn("makeWithDatabase")(function* ( |
| 201 | 206 | }, |
| 202 | 207 | catch: (cause) => |
| 203 | 208 | new SqlError({ |
| 204 | | reason: classifySqliteError(cause, { |
| 205 | | message: "Failed to execute statement", |
| 206 | | operation: "execute", |
| 207 | | }), |
| 209 | reason: classifyError(cause, "Failed to execute statement", "execute"), |
| 208 | 210 | }), |
| 209 | 211 | }), |
| 210 | 212 | (statement) => |
| ... | ... | @@ -216,10 +218,11 @@ const make = Effect.fn("makeWithDatabase")(function* ( |
| 216 | 218 | }, |
| 217 | 219 | catch: (cause) => |
| 218 | 220 | new SqlError({ |
| 219 | | reason: classifySqliteError(cause, { |
| 220 | | message: "Failed to reset statement result mode", |
| 221 | | operation: "resetResultMode", |
| 222 | | }), |
| 221 | reason: classifyError( |
| 222 | cause, |
| 223 | "Failed to reset statement result mode", |
| 224 | "resetResultMode", |
| 225 | ), |
| 223 | 226 | }), |
| 224 | 227 | }).pipe(Effect.orDie), |
| 225 | 228 | ); |
| ... | ... | @@ -273,6 +276,13 @@ const make = Effect.fn("makeWithDatabase")(function* ( |
| 273 | 276 | acquirer, |
| 274 | 277 | compiler, |
| 275 | 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 | 286 | spanAttributes: [ |
| 277 | 287 | ...(options.spanAttributes ? Object.entries(options.spanAttributes) : []), |
| 278 | 288 | [ATTR_DB_SYSTEM_NAME, "sqlite"], |