turbine-orm 0.81.0 → 0.90.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/README.md +26 -25
  2. package/dist/checkout.d.ts +27 -2
  3. package/dist/checkout.js +75 -4
  4. package/dist/cjs/checkout.d.ts +27 -2
  5. package/dist/cjs/checkout.js +76 -3
  6. package/dist/cjs/cli/code-first-tags.d.ts +62 -0
  7. package/dist/cjs/cli/code-first-tags.js +124 -0
  8. package/dist/cjs/cli/compile-query.js +9 -0
  9. package/dist/cjs/cli/config.d.ts +35 -6
  10. package/dist/cjs/cli/config.js +17 -1
  11. package/dist/cjs/cli/destructive.d.ts +7 -4
  12. package/dist/cjs/cli/destructive.js +224 -20
  13. package/dist/cjs/cli/error-catalog.js +36 -0
  14. package/dist/cjs/cli/error-text.d.ts +15 -0
  15. package/dist/cjs/cli/error-text.js +40 -0
  16. package/dist/cjs/cli/index.d.ts +62 -4
  17. package/dist/cjs/cli/index.js +568 -108
  18. package/dist/cjs/cli/local-server-guard.d.ts +53 -0
  19. package/dist/cjs/cli/local-server-guard.js +119 -0
  20. package/dist/cjs/cli/local-server-output.d.ts +41 -0
  21. package/dist/cjs/cli/local-server-output.js +103 -0
  22. package/dist/cjs/cli/mcp.d.ts +8 -0
  23. package/dist/cjs/cli/mcp.js +121 -32
  24. package/dist/cjs/cli/migrate.d.ts +113 -5
  25. package/dist/cjs/cli/migrate.js +270 -21
  26. package/dist/cjs/cli/observe.d.ts +6 -1
  27. package/dist/cjs/cli/observe.js +85 -55
  28. package/dist/cjs/cli/pii-predicate-guard.d.ts +43 -23
  29. package/dist/cjs/cli/pii-predicate-guard.js +98 -37
  30. package/dist/cjs/cli/prisma-resolve.js +17 -13
  31. package/dist/cjs/cli/prisma-schema.js +12 -4
  32. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  33. package/dist/cjs/cli/studio.d.ts +14 -2
  34. package/dist/cjs/cli/studio.js +244 -69
  35. package/dist/cjs/cli/ui.d.ts +1 -1
  36. package/dist/cjs/client.d.ts +558 -85
  37. package/dist/cjs/client.js +1270 -395
  38. package/dist/cjs/connection-guard.d.ts +13 -0
  39. package/dist/cjs/connection-guard.js +28 -0
  40. package/dist/cjs/connection-url.d.ts +51 -16
  41. package/dist/cjs/connection-url.js +67 -34
  42. package/dist/cjs/diagnostics.d.ts +81 -0
  43. package/dist/cjs/diagnostics.js +147 -0
  44. package/dist/cjs/dialect.d.ts +449 -20
  45. package/dist/cjs/dialect.js +117 -76
  46. package/dist/cjs/engine-config.d.ts +7 -6
  47. package/dist/cjs/errors.d.ts +126 -6
  48. package/dist/cjs/errors.js +256 -13
  49. package/dist/cjs/generate.d.ts +57 -7
  50. package/dist/cjs/generate.js +635 -279
  51. package/dist/cjs/global-filters-config.d.ts +59 -0
  52. package/dist/cjs/global-filters-config.js +367 -0
  53. package/dist/cjs/index.d.ts +9 -4
  54. package/dist/cjs/index.js +10 -2
  55. package/dist/cjs/introspect.d.ts +31 -6
  56. package/dist/cjs/introspect.js +108 -50
  57. package/dist/cjs/log-codes.d.ts +12 -0
  58. package/dist/cjs/log-codes.js +77 -0
  59. package/dist/cjs/logger.d.ts +87 -0
  60. package/dist/cjs/logger.js +98 -0
  61. package/dist/cjs/mssql.d.ts +8 -5
  62. package/dist/cjs/mssql.js +352 -65
  63. package/dist/cjs/mysql.d.ts +3 -5
  64. package/dist/cjs/mysql.js +420 -70
  65. package/dist/cjs/nested-write.d.ts +3 -0
  66. package/dist/cjs/nested-write.js +179 -114
  67. package/dist/cjs/pipeline-submittable.d.ts +12 -0
  68. package/dist/cjs/pipeline-submittable.js +118 -44
  69. package/dist/cjs/pipeline.js +21 -26
  70. package/dist/cjs/powdb-introspect.d.ts +5 -2
  71. package/dist/cjs/powdb-introspect.js +20 -15
  72. package/dist/cjs/powdb-shared.d.ts +36 -5
  73. package/dist/cjs/powdb-shared.js +67 -8
  74. package/dist/cjs/powdb.d.ts +4 -0
  75. package/dist/cjs/powdb.js +183 -51
  76. package/dist/cjs/powql.d.ts +394 -51
  77. package/dist/cjs/powql.js +1829 -284
  78. package/dist/cjs/prisma-compat.d.ts +23 -15
  79. package/dist/cjs/prisma-compat.js +507 -372
  80. package/dist/cjs/query/aggregates.d.ts +17 -1
  81. package/dist/cjs/query/aggregates.js +261 -29
  82. package/dist/cjs/query/batched-loader.d.ts +25 -6
  83. package/dist/cjs/query/batched-loader.js +103 -20
  84. package/dist/cjs/query/builder.d.ts +325 -184
  85. package/dist/cjs/query/builder.js +1452 -490
  86. package/dist/cjs/query/column-ref.d.ts +61 -0
  87. package/dist/cjs/query/column-ref.js +73 -0
  88. package/dist/cjs/query/compound-unique.d.ts +21 -0
  89. package/dist/cjs/query/compound-unique.js +98 -18
  90. package/dist/cjs/query/deferred.d.ts +53 -16
  91. package/dist/cjs/query/expr.d.ts +217 -0
  92. package/dist/cjs/query/expr.js +859 -0
  93. package/dist/cjs/query/filters.d.ts +155 -53
  94. package/dist/cjs/query/filters.js +613 -76
  95. package/dist/cjs/query/index.d.ts +2 -0
  96. package/dist/cjs/query/index.js +7 -1
  97. package/dist/cjs/query/json-value.d.ts +43 -0
  98. package/dist/cjs/query/json-value.js +59 -0
  99. package/dist/cjs/query/normalize-args.d.ts +87 -0
  100. package/dist/cjs/query/normalize-args.js +527 -0
  101. package/dist/cjs/query/option-surface.d.ts +16 -4
  102. package/dist/cjs/query/option-surface.js +57 -14
  103. package/dist/cjs/query/relation-names.d.ts +14 -0
  104. package/dist/cjs/query/relation-names.js +77 -18
  105. package/dist/cjs/query/relations.d.ts +30 -3
  106. package/dist/cjs/query/relations.js +219 -63
  107. package/dist/cjs/query/row-lock.d.ts +49 -0
  108. package/dist/cjs/query/row-lock.js +138 -0
  109. package/dist/cjs/query/sql-comment.d.ts +161 -0
  110. package/dist/cjs/query/sql-comment.js +381 -0
  111. package/dist/cjs/query/stream-idle.d.ts +132 -0
  112. package/dist/cjs/query/stream-idle.js +212 -0
  113. package/dist/cjs/query/table-client.d.ts +101 -0
  114. package/dist/cjs/query/table-client.js +74 -0
  115. package/dist/cjs/query/types.d.ts +677 -298
  116. package/dist/cjs/query/utils.d.ts +105 -6
  117. package/dist/cjs/query/utils.js +426 -19
  118. package/dist/cjs/query/warn-registry.d.ts +18 -0
  119. package/dist/cjs/query/warn-registry.js +18 -0
  120. package/dist/cjs/query/where-compile.d.ts +138 -14
  121. package/dist/cjs/query/where-compile.js +513 -29
  122. package/dist/cjs/query/where.d.ts +159 -59
  123. package/dist/cjs/query/where.js +716 -246
  124. package/dist/cjs/query/writes.d.ts +112 -50
  125. package/dist/cjs/query/writes.js +1498 -200
  126. package/dist/cjs/query-events.d.ts +4 -2
  127. package/dist/cjs/query-events.js +3 -1
  128. package/dist/cjs/realtime.d.ts +7 -3
  129. package/dist/cjs/realtime.js +13 -2
  130. package/dist/cjs/schema-builder.d.ts +55 -4
  131. package/dist/cjs/schema-builder.js +382 -18
  132. package/dist/cjs/schema-metadata.d.ts +21 -1
  133. package/dist/cjs/schema-metadata.js +29 -7
  134. package/dist/cjs/schema-sql.d.ts +75 -5
  135. package/dist/cjs/schema-sql.js +497 -45
  136. package/dist/cjs/schema.d.ts +26 -5
  137. package/dist/cjs/schema.js +110 -13
  138. package/dist/cjs/sql-placeholders.d.ts +56 -0
  139. package/dist/cjs/sql-placeholders.js +204 -0
  140. package/dist/cjs/sqlite.d.ts +75 -20
  141. package/dist/cjs/sqlite.js +593 -95
  142. package/dist/cjs/supplied-pool-types.d.ts +48 -0
  143. package/dist/cjs/supplied-pool-types.js +256 -0
  144. package/dist/cjs/transaction-control.d.ts +51 -0
  145. package/dist/cjs/transaction-control.js +316 -0
  146. package/dist/cjs/transaction-state.d.ts +342 -0
  147. package/dist/cjs/transaction-state.js +707 -0
  148. package/dist/cjs/typed-sql.d.ts +37 -3
  149. package/dist/cjs/typed-sql.js +102 -8
  150. package/dist/cli/code-first-tags.d.ts +62 -0
  151. package/dist/cli/code-first-tags.js +120 -0
  152. package/dist/cli/compile-query.js +9 -0
  153. package/dist/cli/config.d.ts +35 -6
  154. package/dist/cli/config.js +16 -1
  155. package/dist/cli/destructive.d.ts +7 -4
  156. package/dist/cli/destructive.js +224 -20
  157. package/dist/cli/error-catalog.js +36 -0
  158. package/dist/cli/error-text.d.ts +15 -0
  159. package/dist/cli/error-text.js +37 -0
  160. package/dist/cli/index.d.ts +62 -4
  161. package/dist/cli/index.js +569 -112
  162. package/dist/cli/local-server-guard.d.ts +53 -0
  163. package/dist/cli/local-server-guard.js +113 -0
  164. package/dist/cli/local-server-output.d.ts +41 -0
  165. package/dist/cli/local-server-output.js +98 -0
  166. package/dist/cli/mcp.d.ts +8 -0
  167. package/dist/cli/mcp.js +121 -34
  168. package/dist/cli/migrate.d.ts +113 -5
  169. package/dist/cli/migrate.js +261 -21
  170. package/dist/cli/observe.d.ts +6 -1
  171. package/dist/cli/observe.js +84 -54
  172. package/dist/cli/pii-predicate-guard.d.ts +43 -23
  173. package/dist/cli/pii-predicate-guard.js +100 -35
  174. package/dist/cli/prisma-resolve.js +15 -11
  175. package/dist/cli/prisma-schema.js +12 -4
  176. package/dist/cli/studio-ui.generated.js +1 -1
  177. package/dist/cli/studio.d.ts +14 -2
  178. package/dist/cli/studio.js +246 -71
  179. package/dist/cli/ui.d.ts +1 -1
  180. package/dist/client.d.ts +558 -85
  181. package/dist/client.js +1275 -400
  182. package/dist/connection-guard.d.ts +13 -0
  183. package/dist/connection-guard.js +27 -0
  184. package/dist/connection-url.d.ts +51 -16
  185. package/dist/connection-url.js +63 -34
  186. package/dist/diagnostics.d.ts +81 -0
  187. package/dist/diagnostics.js +144 -0
  188. package/dist/dialect.d.ts +449 -20
  189. package/dist/dialect.js +115 -43
  190. package/dist/engine-config.d.ts +7 -6
  191. package/dist/errors.d.ts +126 -6
  192. package/dist/errors.js +252 -12
  193. package/dist/generate.d.ts +57 -7
  194. package/dist/generate.js +635 -281
  195. package/dist/global-filters-config.d.ts +59 -0
  196. package/dist/global-filters-config.js +361 -0
  197. package/dist/index.d.ts +9 -4
  198. package/dist/index.js +4 -2
  199. package/dist/introspect.d.ts +31 -6
  200. package/dist/introspect.js +106 -50
  201. package/dist/log-codes.d.ts +12 -0
  202. package/dist/log-codes.js +74 -0
  203. package/dist/logger.d.ts +87 -0
  204. package/dist/logger.js +93 -0
  205. package/dist/mssql.d.ts +8 -5
  206. package/dist/mssql.js +354 -67
  207. package/dist/mysql.d.ts +3 -5
  208. package/dist/mysql.js +423 -73
  209. package/dist/nested-write.d.ts +3 -0
  210. package/dist/nested-write.js +177 -112
  211. package/dist/pipeline-submittable.d.ts +12 -0
  212. package/dist/pipeline-submittable.js +117 -44
  213. package/dist/pipeline.js +21 -26
  214. package/dist/powdb-introspect.d.ts +5 -2
  215. package/dist/powdb-introspect.js +21 -16
  216. package/dist/powdb-shared.d.ts +36 -5
  217. package/dist/powdb-shared.js +65 -8
  218. package/dist/powdb.d.ts +4 -0
  219. package/dist/powdb.js +183 -51
  220. package/dist/powql.d.ts +394 -51
  221. package/dist/powql.js +1836 -291
  222. package/dist/prisma-compat.d.ts +23 -15
  223. package/dist/prisma-compat.js +508 -373
  224. package/dist/query/aggregates.d.ts +17 -1
  225. package/dist/query/aggregates.js +263 -32
  226. package/dist/query/batched-loader.d.ts +25 -6
  227. package/dist/query/batched-loader.js +101 -21
  228. package/dist/query/builder.d.ts +325 -184
  229. package/dist/query/builder.js +1458 -495
  230. package/dist/query/column-ref.d.ts +61 -0
  231. package/dist/query/column-ref.js +66 -0
  232. package/dist/query/compound-unique.d.ts +21 -0
  233. package/dist/query/compound-unique.js +97 -19
  234. package/dist/query/deferred.d.ts +53 -16
  235. package/dist/query/expr.d.ts +217 -0
  236. package/dist/query/expr.js +848 -0
  237. package/dist/query/filters.d.ts +155 -53
  238. package/dist/query/filters.js +597 -72
  239. package/dist/query/index.d.ts +2 -0
  240. package/dist/query/index.js +4 -0
  241. package/dist/query/json-value.d.ts +43 -0
  242. package/dist/query/json-value.js +54 -0
  243. package/dist/query/normalize-args.d.ts +87 -0
  244. package/dist/query/normalize-args.js +517 -0
  245. package/dist/query/option-surface.d.ts +16 -4
  246. package/dist/query/option-surface.js +56 -13
  247. package/dist/query/relation-names.d.ts +14 -0
  248. package/dist/query/relation-names.js +77 -19
  249. package/dist/query/relations.d.ts +30 -3
  250. package/dist/query/relations.js +220 -67
  251. package/dist/query/row-lock.d.ts +49 -0
  252. package/dist/query/row-lock.js +131 -0
  253. package/dist/query/sql-comment.d.ts +161 -0
  254. package/dist/query/sql-comment.js +374 -0
  255. package/dist/query/stream-idle.d.ts +132 -0
  256. package/dist/query/stream-idle.js +203 -0
  257. package/dist/query/table-client.d.ts +101 -0
  258. package/dist/query/table-client.js +70 -0
  259. package/dist/query/types.d.ts +677 -298
  260. package/dist/query/utils.d.ts +105 -6
  261. package/dist/query/utils.js +414 -18
  262. package/dist/query/warn-registry.d.ts +18 -0
  263. package/dist/query/warn-registry.js +18 -0
  264. package/dist/query/where-compile.d.ts +138 -14
  265. package/dist/query/where-compile.js +508 -31
  266. package/dist/query/where.d.ts +159 -59
  267. package/dist/query/where.js +711 -248
  268. package/dist/query/writes.d.ts +112 -50
  269. package/dist/query/writes.js +1496 -202
  270. package/dist/query-events.d.ts +4 -2
  271. package/dist/query-events.js +3 -1
  272. package/dist/realtime.d.ts +7 -3
  273. package/dist/realtime.js +13 -2
  274. package/dist/schema-builder.d.ts +55 -4
  275. package/dist/schema-builder.js +381 -18
  276. package/dist/schema-metadata.d.ts +21 -1
  277. package/dist/schema-metadata.js +28 -7
  278. package/dist/schema-sql.d.ts +75 -5
  279. package/dist/schema-sql.js +494 -44
  280. package/dist/schema.d.ts +26 -5
  281. package/dist/schema.js +109 -13
  282. package/dist/sql-placeholders.d.ts +56 -0
  283. package/dist/sql-placeholders.js +200 -0
  284. package/dist/sqlite.d.ts +75 -20
  285. package/dist/sqlite.js +597 -99
  286. package/dist/supplied-pool-types.d.ts +48 -0
  287. package/dist/supplied-pool-types.js +247 -0
  288. package/dist/transaction-control.d.ts +51 -0
  289. package/dist/transaction-control.js +311 -0
  290. package/dist/transaction-state.d.ts +342 -0
  291. package/dist/transaction-state.js +696 -0
  292. package/dist/typed-sql.d.ts +37 -3
  293. package/dist/typed-sql.js +100 -8
  294. package/package.json +7 -4
  295. package/skills/turbine-orm/SKILL.md +56 -6
@@ -20,18 +20,24 @@
20
20
  * - `RETURNING` + `ON CONFLICT … DO UPDATE` (SQLite ≥ 3.35) → create / upsert /
21
21
  * update / delete return real rows in a single statement (`resultStrategy =
22
22
  * 'returning'`, same as Postgres).
23
- * - **Single-writer:** one connection / one write transaction at a time;
24
- * concurrent writers get `SQLITE_BUSY` (treated as retryable). `journal_mode =
25
- * WAL` is enabled for file databases to allow concurrent readers.
23
+ * - **Single-writer:** one connection / one transaction at a time. On one
24
+ * client, statements from other callers wait (FIFO, up to `busyTimeoutMs`)
25
+ * while a transaction is open instead of running inside it; another
26
+ * process's writer gets `SQLITE_BUSY`. Both surface as the retryable
27
+ * `SerializationFailureError`. `journal_mode = WAL` is enabled for file
28
+ * databases to allow concurrent readers across connections.
26
29
  * - **Unsupported (throw `UnsupportedFeatureError`):** pgvector distance ops,
27
30
  * LISTEN/NOTIFY (`$listen` / `$notify`), RLS `sessionContext`. Advisory-lock
28
31
  * migration locking is unavailable, SQLite is single-writer, so migrations
29
32
  * serialize naturally.
30
- * - **Type affinity caveats:** SQLite has no native `BOOLEAN` (0/1 integers) or
31
- * `DATE` (TEXT/INTEGER). Booleans bind as 1/0; `Date` values bind as ISO-8601
32
- * text; columns declared `TIMESTAMP`/`DATETIME`/`DATE` are coerced back to
33
- * `Date`. Integers wider than `Number.MAX_SAFE_INTEGER` come back as strings
34
- * (the same safe-int policy Turbine uses for Postgres `int8`).
33
+ * - **Type affinity caveats:** SQLite has no native `BOOLEAN` (0/1 integers),
34
+ * `JSON` (text) or `DATE` (TEXT/INTEGER). Booleans bind as 1/0; `Date` values
35
+ * bind as ISO-8601 text; an integral JS number binds as INTEGER. A column the
36
+ * schema declares boolean or json/jsonb reads back as a boolean or a parsed
37
+ * value on every path; date/time columns are coerced back to `Date`.
38
+ * Integers wider than `Number.MAX_SAFE_INTEGER` come back as strings (the
39
+ * same safe-int policy Turbine uses for Postgres `int8`). `numeric` has no
40
+ * exact storage: it reads back as a JS number.
35
41
  * - **Case-insensitive matching** uses `COLLATE NOCASE`, which is **ASCII-only**
36
42
  * (no Unicode case folding).
37
43
  *
@@ -69,7 +75,7 @@
69
75
  *
70
76
  * const db = turbineSqlite(':memory:', schemaDefToMetadata(schema));
71
77
  * for (const stmt of schemaToSQL(schema, { dialect: sqliteDialect })) {
72
- * await db.raw([stmt] as never);
78
+ * await db.rawUnsafe(stmt);
73
79
  * }
74
80
  *
75
81
  * // `db.table(...)`, not `db.users`: the typed property accessors are emitted
@@ -84,13 +90,16 @@ exports.sqliteTypeToTs = sqliteTypeToTs;
84
90
  exports.introspectSqliteDatabase = introspectSqliteDatabase;
85
91
  exports.introspectSqlite = introspectSqlite;
86
92
  exports.turbineSqlite = turbineSqlite;
93
+ const node_async_hooks_1 = require("node:async_hooks");
87
94
  const node_module_1 = require("node:module");
88
95
  const client_js_1 = require("./client.js");
89
96
  const dialect_js_1 = require("./dialect.js");
90
97
  const errors_js_1 = require("./errors.js");
91
98
  const introspect_js_1 = require("./introspect.js");
99
+ const sql_comment_js_1 = require("./query/sql-comment.js");
92
100
  const utils_js_1 = require("./query/utils.js");
93
101
  const schema_js_1 = require("./schema.js");
102
+ const sql_placeholders_js_1 = require("./sql-placeholders.js");
94
103
  let cachedDatabaseSync;
95
104
  /**
96
105
  * Lazily load `node:sqlite`'s `DatabaseSync` constructor.
@@ -128,7 +137,8 @@ function loadDatabaseSync() {
128
137
  * Coerce an arbitrary JS value into something `node:sqlite` can bind.
129
138
  *
130
139
  * `node:sqlite` throws on `boolean`, `undefined`, `Date`, and plain objects, so
131
- * we normalize: booleans → 1/0, `undefined`/`null` → NULL, `Date` → ISO text,
140
+ * we normalize: booleans → 1/0, a safe integer → INTEGER (see below),
141
+ * `undefined`/`null` → NULL, `Date` → ISO text,
132
142
  * `Uint8Array`/`Buffer` → BLOB, and any remaining object/array → JSON text
133
143
  * (matches how Turbine already pre-serializes JSON filter params).
134
144
  */
@@ -139,6 +149,12 @@ function toSqliteParam(value) {
139
149
  case 'boolean':
140
150
  return value ? 1 : 0;
141
151
  case 'number':
152
+ // `node:sqlite` binds every JS number as REAL, so `views / 2` divided as
153
+ // floats (75 / 2 = 37.5, stored in an INTEGER column) and an integral
154
+ // number written to a TEXT column was stored as '3.0'. Binding a safe
155
+ // integer as an INTEGER gives the arithmetic and text PostgreSQL gives.
156
+ // -0 stays REAL: as an integer it would lose its sign.
157
+ return Number.isSafeInteger(value) && !Object.is(value, -0) ? BigInt(value) : value;
142
158
  case 'bigint':
143
159
  case 'string':
144
160
  return value;
@@ -149,6 +165,44 @@ function toSqliteParam(value) {
149
165
  return value; // also covers Buffer
150
166
  return JSON.stringify(value);
151
167
  }
168
+ /**
169
+ * The JSON text an `in` / `notIn` list binds as, element by element in the
170
+ * form {@link toSqliteParam} would bind it, so a list element compares exactly
171
+ * as the same value given to `equals` does. A bigint is written as a JSON
172
+ * integer (exact to 64 bits; `JSON.stringify` refuses one), an infinity as the
173
+ * overflowing literal SQLite's JSON parser reads as one, and a binary value as
174
+ * `{"b": "<hex>"}`, which {@link sqliteDialect.buildInClause} turns back into a
175
+ * BLOB. Any other object binds as its JSON text, as it would on its own.
176
+ */
177
+ function sqliteInListJson(values) {
178
+ const parts = values.map((value) => {
179
+ if (value === undefined || value === null)
180
+ return 'null';
181
+ switch (typeof value) {
182
+ case 'boolean':
183
+ return value ? '1' : '0';
184
+ case 'number':
185
+ if (Number.isFinite(value))
186
+ return JSON.stringify(value);
187
+ if (Number.isNaN(value))
188
+ return 'null';
189
+ return value > 0 ? '9e999' : '-9e999';
190
+ case 'bigint':
191
+ return value.toString();
192
+ case 'string':
193
+ return JSON.stringify(value);
194
+ }
195
+ if (value instanceof Uint8Array) {
196
+ let hex = '';
197
+ for (const byte of value)
198
+ hex += byte.toString(16).padStart(2, '0');
199
+ return `{"b":"${hex}"}`;
200
+ }
201
+ const bound = toSqliteParam(value);
202
+ return JSON.stringify(bound);
203
+ });
204
+ return `[${parts.join(',')}]`;
205
+ }
152
206
  /**
153
207
  * Normalize a single column value read from SQLite. With `setReadBigInts(true)`
154
208
  * every integer column comes back as a `bigint`; apply the same safe-integer
@@ -163,44 +217,142 @@ function normalizeValue(value) {
163
217
  }
164
218
  return value;
165
219
  }
220
+ /**
221
+ * The decode a column of `type` needs on SQLite, or `undefined`. `type` is a
222
+ * declared SQLite type or a schema type (`bool`, `jsonb`). The ONE answer both
223
+ * read paths use: the driver shim for direct reads and {@link sqliteDialect}'s
224
+ * `jsonWireRule` for `with` rows, so the two cannot disagree about a column.
225
+ */
226
+ function sqliteValueDecode(type) {
227
+ const t = type.toUpperCase();
228
+ if (t.includes('BOOL'))
229
+ return 'boolean';
230
+ if (t === 'JSON' || t === 'JSONB')
231
+ return 'json';
232
+ return undefined;
233
+ }
234
+ function declaredDecoders(schema) {
235
+ if (!schema)
236
+ return null;
237
+ let out = null;
238
+ for (const [table, meta] of Object.entries(schema.tables)) {
239
+ for (const column of meta.columns) {
240
+ const type = column.pgType ?? meta.pgTypes?.[column.name];
241
+ const decode = typeof type === 'string' ? sqliteValueDecode(type) : undefined;
242
+ if (decode === undefined)
243
+ continue;
244
+ if (out === null)
245
+ out = new Map();
246
+ out.set(`${table}\u0000${column.name}`, decode);
247
+ }
248
+ }
249
+ return out;
250
+ }
251
+ /**
252
+ * A decimal integer or float as SQLite prints it (`printf('%!.17g')` / `CAST(... AS TEXT)`).
253
+ *
254
+ * Linear by construction: the fraction's digits can only follow a literal `.`,
255
+ * so a run of digits has exactly one way to match. The earlier `\d+\.?\d*`
256
+ * could split a dot-less run between its two quantifiers in every possible
257
+ * place, which made a long non-numeric cell take seconds to reject (quadratic:
258
+ * 40,000 digits and an `x` took 0.8 s, a million take 1.5 ms now).
259
+ */
260
+ const SQLITE_NUMBER_TEXT = /^-?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
261
+ /**
262
+ * The text SQLite's INTEGER, REAL and NUMERIC affinities convert to a number,
263
+ * measured against SQLite 3.51: an optional sign, a decimal integer or float,
264
+ * an optional exponent, and ASCII whitespace around it. Anything else
265
+ * (`'abc'`, `''`, `'Infinity'`, `'0x10'`, `'1_000'`) is stored as TEXT in the
266
+ * numeric column. Linear for the same reason as {@link SQLITE_NUMBER_TEXT}.
267
+ */
268
+ const SQLITE_NUMERIC_LITERAL = /^[ \t\n\v\f\r]*[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?[ \t\n\v\f\r]*$/i;
269
+ /**
270
+ * Declared column types whose SQLite affinity is numeric (INTEGER, REAL or
271
+ * NUMERIC), boolean included (stored 1/0). Matched whole, not by SQLite's own
272
+ * "contains INT" rule: the type a schema records for a column can be an enum's
273
+ * name (`point_kind`), which that rule reads as INTEGER while the column is
274
+ * TEXT. A type this misses is written as SQLite always wrote it, so a miss
275
+ * costs a refusal, never a wrong one.
276
+ */
277
+ const SQLITE_NUMERIC_COLUMN_TYPE = /^(?:(?:tiny|small|medium|big)?int(?:eger)?|int[248]|(?:small|big)?serial|unsigned big int|real|float[48]?|double(?: precision)?|numeric|decimal|money|bool(?:ean)?)(?:\(\s*\d+\s*(?:,\s*\d+\s*)?\))?$/i;
278
+ /**
279
+ * Decode a REAL / NUMERIC cell carried as text through a `with` join back to
280
+ * the value a direct read returns: an integer by the safe-integer policy, a
281
+ * double as the number its 17 digits name, `Inf` / `-Inf` as infinities.
282
+ */
283
+ function decodeRealText(value) {
284
+ if (typeof value !== 'string')
285
+ return value;
286
+ if (/^-?\d+$/.test(value)) {
287
+ const asNumber = Number(value);
288
+ return Number.isSafeInteger(asNumber) ? asNumber : value;
289
+ }
290
+ if (value === 'Inf')
291
+ return Number.POSITIVE_INFINITY;
292
+ if (value === '-Inf')
293
+ return Number.NEGATIVE_INFINITY;
294
+ return SQLITE_NUMBER_TEXT.test(value) ? Number(value) : value;
295
+ }
296
+ /** Read a JSON column's stored text as its value; text that is not JSON is returned as stored. */
297
+ function parseJsonCell(value) {
298
+ if (typeof value !== 'string')
299
+ return value;
300
+ try {
301
+ return JSON.parse(value);
302
+ }
303
+ catch {
304
+ return value;
305
+ }
306
+ }
166
307
  /**
167
308
  * Convert a `node:sqlite` null-prototype row into a normalized plain object.
168
309
  *
169
- * `booleanKeys`, when present, names the result columns whose ORIGIN column is
170
- * declared with a boolean affinity; their 1/0 storage is turned back into
171
- * `true`/`false` (see {@link booleanResultColumns}). Anything else in such a
172
- * column, and every column not listed, passes through untouched.
310
+ * `decoders`, when present, names the result columns whose ORIGIN column is a
311
+ * boolean or a JSON column (see {@link resultColumnDecoders}): 1/0 becomes
312
+ * `true`/`false`, JSON text becomes its value. Anything else in such a column,
313
+ * and every column not listed, passes through untouched.
173
314
  */
174
- function normalizeRow(row, booleanKeys) {
315
+ function normalizeRow(row, decoders) {
175
316
  const out = {};
176
317
  for (const key of Object.keys(row)) {
177
318
  const value = normalizeValue(row[key]);
178
- out[key] = booleanKeys?.has(key) && (value === 1 || value === 0) ? value === 1 : value;
319
+ const decode = decoders?.get(key);
320
+ out[key] =
321
+ decode === undefined
322
+ ? value
323
+ : decode === 'json'
324
+ ? parseJsonCell(value)
325
+ : value === 1 || value === 0
326
+ ? value === 1
327
+ : value;
179
328
  }
180
329
  return out;
181
330
  }
182
331
  /**
183
- * The result columns of a prepared statement whose ORIGIN column is declared
184
- * boolean, or `null` when there are none (the overwhelmingly common case).
332
+ * The result columns of a prepared statement that need decoding, or `null`
333
+ * when there are none (the overwhelmingly common case).
185
334
  *
186
- * WHY THIS EXISTS. SQLite has no boolean storage class: a `BOOLEAN` column
187
- * holds 1/0 integers. Turbine still generates `ok: boolean` for it
188
- * ({@link sqliteTypeToTs} maps `/bool/` to `boolean`), so `row.ok === true` was
189
- * false, `JSON.stringify(row)` emitted `1`, and a value the ORM itself wrote as
190
- * `true` came back as a number. The declared type is the ONLY thing that says
191
- * "this integer is a boolean", and it lives in the schema, not in the value.
335
+ * WHY THIS EXISTS. SQLite has no boolean or JSON storage class: a boolean is
336
+ * stored as 1/0 and a JSON document as text. Turbine still generates
337
+ * `ok: boolean` and a JSON value for those columns, and PostgreSQL returns
338
+ * exactly that, so a value the ORM itself wrote as `true` or `{ a: 1 }` came
339
+ * back as `1` or `'{"a":1}'`.
192
340
  *
193
- * The decltype comes from the driver rather than from Turbine's metadata
194
- * because the driver knows which TABLE each result column came from.
195
- * `StatementSync.columns()` reports `type` as `sqlite3_column_decltype`, which
196
- * is non-null only for a direct table-column reference and null for any
197
- * expression, so a computed column can never be coerced by accident.
341
+ * Two sources name such a column, both read from the driver's per-column
342
+ * description, because only the driver knows which TABLE a result column came
343
+ * from. `StatementSync.columns()` reports the declared type
344
+ * (`sqlite3_column_decltype`) and the origin table and column, all of which are
345
+ * null for an expression, so a computed column is never decoded by accident.
346
+ * - A declared type naming a boolean (`BOOLEAN`), as before.
347
+ * - The client's schema: an origin column declared boolean or json/jsonb there.
348
+ * Turbine's own DDL stores those as INTEGER and TEXT, so the declared type
349
+ * alone missed every table Turbine created.
198
350
  *
199
- * Node gained `columns()` in 22.13 / 23.4 while this engine's floor is 22.5, so
200
- * it is feature-detected: on the narrow band without it, booleans keep reading
201
- * back as 1/0 exactly as before rather than the engine refusing to run.
351
+ * Node gained `columns()` in 22.16 / 23.11 while this engine's floor is 22.5, so
352
+ * it is feature-detected: on the narrow band without it, these columns keep
353
+ * reading back as stored rather than the engine refusing to run.
202
354
  */
203
- function booleanResultColumns(stmt) {
355
+ function resultColumnDecoders(stmt, declared) {
204
356
  const columnsFn = stmt.columns;
205
357
  if (typeof columnsFn !== 'function')
206
358
  return null;
@@ -210,18 +362,24 @@ function booleanResultColumns(stmt) {
210
362
  }
211
363
  catch {
212
364
  // A statement the driver cannot describe (some PRAGMA forms) is not worth
213
- // failing a query over; skip the coercion for it.
365
+ // failing a query over; skip the decoding for it.
214
366
  return null;
215
367
  }
216
368
  let out = null;
217
369
  for (const c of described) {
218
- if (typeof c.name !== 'string' || typeof c.type !== 'string')
370
+ if (typeof c.name !== 'string')
219
371
  continue;
220
- if (!/bool/i.test(c.type))
372
+ let decode;
373
+ if (typeof c.type === 'string' && sqliteValueDecode(c.type) === 'boolean')
374
+ decode = 'boolean';
375
+ else if (declared !== null && typeof c.table === 'string' && typeof c.column === 'string') {
376
+ decode = declared.get(`${c.table}\u0000${c.column}`);
377
+ }
378
+ if (decode === undefined)
221
379
  continue;
222
380
  if (out === null)
223
- out = new Set();
224
- out.add(c.name);
381
+ out = new Map();
382
+ out.set(c.name, decode);
225
383
  }
226
384
  return out;
227
385
  }
@@ -235,11 +393,11 @@ let cachedSqliteNamespace;
235
393
  /**
236
394
  * Can the driver report a result column's DECLARED type?
237
395
  *
238
- * This is the same capability {@link booleanResultColumns} feature-detects, and
396
+ * This is the same capability {@link resultColumnDecoders} feature-detects, and
239
397
  * the two have to be asked the SAME question or they split: that helper is what
240
398
  * turns 1/0 into `true`/`false` on every direct read, and the boolean
241
399
  * `jsonWireRule` is what does it on the join path. On the Node band with
242
- * `node:sqlite` but without `StatementSync.columns()` (>= 22.5, < 22.13 / 23.4)
400
+ * `node:sqlite` but without `StatementSync.columns()` (>= 22.5, < 22.16 / 23.11)
243
401
  * the helper returns `null` and every direct read keeps 1/0, so a join that
244
402
  * still converted would produce exactly the strategy-dependent type flip the
245
403
  * rule exists to prevent, on the one runtime where nothing else can catch it.
@@ -269,7 +427,12 @@ const ROW_RETURNING_HEAD = /^\s*(?:select|with|pragma|explain|values)\b/i;
269
427
  * A `RETURNING` clause turns any write into a row-producing statement.
270
428
  */
271
429
  function statementReturnsRows(sql) {
272
- return ROW_RETURNING_HEAD.test(sql) || /\breturning\b/i.test(sql);
430
+ // A `sqlComment` (query/sql-comment.ts) sits after the statement and its
431
+ // values are data, so the RETURNING test reads the statement without it: a
432
+ // value such as `orders-returning` must not turn a write into a row read
433
+ // and its change count into 0.
434
+ const code = (0, sql_comment_js_1.withoutTrailingComment)(sql);
435
+ return ROW_RETURNING_HEAD.test(code) || /\breturning\b/i.test(code);
273
436
  }
274
437
  /**
275
438
  * Augment a `node:sqlite` error with the Postgres-shaped fields that
@@ -341,18 +504,34 @@ function augmentSqliteError(err) {
341
504
  target.code = '23514';
342
505
  return err;
343
506
  default:
344
- // SQLITE_BUSY (5) / SQLITE_LOCKED (6) / SQLITE_BUSY_SNAPSHOT (261) etc.
345
- if (primary === 5 || primary === 6) {
346
- // Map to serialization_failure so withRetry()/$retry() retry it.
347
- target.code = '40001';
348
- return err;
349
- }
507
+ // SQLITE_BUSY (5) / SQLITE_LOCKED (6) and their extended codes
508
+ // (SQLITE_BUSY_SNAPSHOT 517, SQLITE_LOCKED_SHAREDCACHE 262, ...). Built as
509
+ // the typed retryable error HERE rather than annotated, because the raw
510
+ // paths (`db.transaction(client => ...)`, `db.pool.query`) never pass
511
+ // through `wrapPgError` and would otherwise hand back the driver error.
512
+ if (primary === 5 || primary === 6)
513
+ return sqliteBusyError(e.errcode, message, err);
350
514
  // SQLITE_ERROR (1): the statement itself is wrong. See the note above.
351
515
  if (primary === 1)
352
516
  return sqliteLogicError(message, err);
353
517
  return err;
354
518
  }
355
519
  }
520
+ /**
521
+ * A `SQLITE_BUSY` / `SQLITE_LOCKED` failure as the retryable typed error, so
522
+ * `withRetry` / `$retry` and `err.isRetryable` handle it on every path.
523
+ * `busy_timeout` does not apply to a deferred transaction that read first and
524
+ * then tried to write (SQLITE_BUSY_SNAPSHOT): only retrying the WHOLE
525
+ * transaction can succeed, which is what the hint says.
526
+ */
527
+ function sqliteBusyError(errcode, message, cause) {
528
+ const kind = (errcode & 0xff) === 5 ? 'SQLITE_BUSY' : 'SQLITE_LOCKED';
529
+ return new errors_js_1.SerializationFailureError({
530
+ message: `SQLite database is busy (${kind}, code ${errcode}): ${message}. Another connection holds the lock it needs. ` +
531
+ 'Retry the whole transaction (withRetry does), or raise busyTimeoutMs.',
532
+ cause,
533
+ });
534
+ }
356
535
  /**
357
536
  * The `ValidationError` for a `SQLITE_ERROR`, with a hint for the two spellings
358
537
  * that account for nearly all of them.
@@ -370,17 +549,17 @@ function augmentSqliteError(err) {
370
549
  function sqliteLogicError(message, cause) {
371
550
  const missingTable = /^no such table:\s*(\S+)/i.exec(message);
372
551
  if (missingTable) {
373
- return new errors_js_1.ValidationError(`SQLite has no table "${missingTable[1]}": ${message}. ` +
552
+ return new errors_js_1.ValidationError(`SQLite has no table "${(0, introspect_js_1.printableCatalogName)(missingTable[1])}": ${(0, introspect_js_1.printableCatalogName)(message)}. ` +
374
553
  'Create it first (run your migrations, or execute the CREATE TABLE statements for this schema); ' +
375
554
  'an in-memory database starts empty on every connection.', { cause });
376
555
  }
377
556
  const missingColumn = /^no such column:\s*(\S+)/i.exec(message);
378
557
  if (missingColumn) {
379
- return new errors_js_1.ValidationError(`SQLite has no column "${missingColumn[1]}": ${message}. ` +
558
+ return new errors_js_1.ValidationError(`SQLite has no column "${(0, introspect_js_1.printableCatalogName)(missingColumn[1])}": ${(0, introspect_js_1.printableCatalogName)(message)}. ` +
380
559
  'Check the spelling against the table as it exists in this database, ' +
381
560
  'and re-run `turbine generate` if the schema has changed.', { cause });
382
561
  }
383
- return new errors_js_1.ValidationError(`SQLite rejected the statement: ${message}`, { cause });
562
+ return new errors_js_1.ValidationError(`SQLite rejected the statement: ${(0, introspect_js_1.printableCatalogName)(message)}`, { cause });
384
563
  }
385
564
  function normalizeQueryArgs(arg, values) {
386
565
  if (typeof arg === 'string')
@@ -410,8 +589,15 @@ function toNamedBinding(values) {
410
589
  }
411
590
  return named;
412
591
  }
413
- function runStatement(db, sql, values, booleanCache) {
592
+ /** The refusal for a placeholder the shim cannot bind (see rewritePositionalPlaceholders). */
593
+ function refusePlaceholder(message) {
594
+ throw new errors_js_1.ValidationError(message);
595
+ }
596
+ function runStatement(db, text, values, decoderCache, declared = null) {
414
597
  const binding = toNamedBinding(values);
598
+ // `$n` is the spelling `rawUnsafe` documents for every engine; SQLite would
599
+ // read `$1` as a parameter NAMED "1", which the `{ p1, ... }` binding lacks.
600
+ const sql = (0, sql_placeholders_js_1.rewritePositionalPlaceholders)(text, 'sqlite', values.length, (i) => `:p${i}`, refusePlaceholder);
415
601
  let stmt;
416
602
  try {
417
603
  stmt = db.prepare(sql);
@@ -422,15 +608,15 @@ function runStatement(db, sql, values, booleanCache) {
422
608
  stmt.setReadBigInts(true);
423
609
  try {
424
610
  if (statementReturnsRows(sql)) {
425
- // Describe each distinct statement ONCE. `null` (no boolean column) is a
611
+ // Describe each distinct statement ONCE. `null` (nothing to decode) is a
426
612
  // cached answer too, so the ordinary query pays a single Map hit per call
427
613
  // and nothing per row.
428
- let booleanKeys = booleanCache?.get(sql);
429
- if (booleanKeys === undefined) {
430
- booleanKeys = booleanResultColumns(stmt);
431
- booleanCache?.set(sql, booleanKeys);
614
+ let decoders = decoderCache?.get(sql);
615
+ if (decoders === undefined) {
616
+ decoders = resultColumnDecoders(stmt, declared);
617
+ decoderCache?.set(sql, decoders);
432
618
  }
433
- const rows = (binding ? stmt.all(binding) : stmt.all()).map((r) => normalizeRow(r, booleanKeys ?? undefined));
619
+ const rows = (binding ? stmt.all(binding) : stmt.all()).map((r) => normalizeRow(r, decoders ?? undefined));
434
620
  return { rows, rowCount: rows.length };
435
621
  }
436
622
  const info = binding ? stmt.run(binding) : stmt.run();
@@ -442,42 +628,96 @@ function runStatement(db, sql, values, booleanCache) {
442
628
  throw augmentSqliteError(err);
443
629
  }
444
630
  }
631
+ /** Default wait, in ms, for a transaction another caller holds open on the handle. */
632
+ const DEFAULT_BUSY_TIMEOUT_MS = 5000;
633
+ const sqliteTxStorage = new node_async_hooks_1.AsyncLocalStorage();
634
+ /** Statement heads that leave the handle inside a transaction when they succeed. */
635
+ const TX_OPEN_HEAD = /^\s*(?:begin|savepoint)\b/i;
636
+ /** Statement heads that end the transaction (not `ROLLBACK TO`, which keeps it open). */
637
+ const TX_CLOSE_HEAD = /^\s*(?:commit|end|rollback(?!\s+(?:transaction\s+)?to\b))\b/i;
638
+ /**
639
+ * The one statement the pool can never run: something on the handle while the
640
+ * transaction that owns it is waiting for that very statement.
641
+ */
642
+ function statementInsideTransactionError() {
643
+ return new errors_js_1.UnsupportedFeatureError('A statement on the client while its own transaction is open', 'sqlite', 'This call was made through the client, not the `tx` client, from inside an open $transaction callback. ' +
644
+ 'SQLite has one connection here, so the call would either join that transaction (and be rolled back with ' +
645
+ 'it) or wait for it forever. Use the `tx` client the callback receives, or make the call after the ' +
646
+ 'transaction completes.');
647
+ }
648
+ /** The error a caller gets when an open transaction holds the handle past `busyTimeoutMs`. */
649
+ function handleBusyError(timeoutMs) {
650
+ return new errors_js_1.SerializationFailureError({
651
+ message: `SQLite database is busy: a transaction on this client held it for longer than busyTimeoutMs (${timeoutMs} ms). ` +
652
+ 'Retry the operation (withRetry does), keep transactions short, or raise busyTimeoutMs.',
653
+ });
654
+ }
445
655
  /**
446
656
  * A `PgCompatPool` backed by a single `node:sqlite` `DatabaseSync` connection.
447
657
  * SQLite is single-connection by nature (a `:memory:` database is per-handle),
448
- * so `connect()` hands back a client over the **same** handle, transactions
449
- * (`BEGIN`/`COMMIT`/`ROLLBACK`, `SAVEPOINT` nesting) just run on it. Queries are
450
- * serialized; this is the documented single-writer model.
658
+ * so `connect()` hands back a client over the **same** handle.
659
+ *
660
+ * One handle carries one transaction, so the pool decides who may use it. A
661
+ * caller whose statement opens a transaction OWNS the handle until that
662
+ * transaction ends; every other caller's statements, plain queries included,
663
+ * wait their turn in FIFO order (bounded by `busyTimeoutMs`) instead of running
664
+ * inside it. Without that, a write issued outside an open transaction joined
665
+ * it, reported success, and was rolled back with it, and reads saw its
666
+ * uncommitted rows. A checkout that does not open a transaction (a stream, a
667
+ * connectivity check) holds nothing, so other callers keep running beside it.
451
668
  */
452
669
  class SqlitePool {
453
670
  /** The underlying `node:sqlite` handle, exposed as an escape hatch (seed/DDL). */
454
671
  db;
455
672
  closed = false;
456
673
  /**
457
- * `sql -> declared-boolean result columns` (or `null` for none). Bounded like
674
+ * `sql -> result columns to decode` (or `null` for none). Bounded like
458
675
  * the query builder's SQL-template cache, since a caller can generate
459
676
  * unbounded distinct statements (raw SQL, an IN-list that inlines).
460
677
  */
461
- booleanCache = new utils_js_1.LRUCache(1000);
462
- constructor(db) {
678
+ decoderCache = new utils_js_1.LRUCache(1000);
679
+ /** Schema columns whose stored form is not their declared JS type (see {@link declaredDecoders}). */
680
+ declared;
681
+ busyTimeoutMs;
682
+ /** Who may run statements on the handle right now; `null` when nobody holds it. */
683
+ owner = null;
684
+ waiters = [];
685
+ /** The owner for statements sent through `pool.query()` rather than a checkout. */
686
+ poolOwner = {};
687
+ /** Transaction state when the driver cannot report it (`isTransaction` arrived in Node 22.16). */
688
+ trackedTransaction = false;
689
+ constructor(db, options = {}) {
463
690
  this.db = db;
691
+ const t = Number(options.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS);
692
+ this.busyTimeoutMs = Number.isFinite(t) && t >= 0 ? t : DEFAULT_BUSY_TIMEOUT_MS;
693
+ this.declared = declaredDecoders(options.schema);
464
694
  }
465
695
  // biome-ignore lint/suspicious/noExplicitAny: pg-compat query is generic over the row shape; runStatement returns plain objects.
466
696
  async query(text, values) {
467
697
  const { text: sql, params } = normalizeQueryArgs(text, values);
468
- return runStatement(this.db, sql, params, this.booleanCache);
698
+ return this.runAs(this.poolOwner, sql, params);
469
699
  }
470
700
  async connect() {
471
- const db = this.db;
472
- const booleanCache = this.booleanCache;
701
+ if (this.closed)
702
+ throw new errors_js_1.ConnectionError('This SQLite client has been disconnected.');
703
+ const owner = {};
704
+ let released = false;
473
705
  return {
474
706
  // biome-ignore lint/suspicious/noExplicitAny: see query() above.
475
707
  query: async (text, values) => {
708
+ if (released) {
709
+ throw new errors_js_1.ConnectionError('This SQLite connection was released (its transaction ended or timed out); ' +
710
+ 'run the statement through the client or a new transaction.');
711
+ }
476
712
  const { text: sql, params } = normalizeQueryArgs(text, values);
477
- return runStatement(db, sql, params, booleanCache);
713
+ return this.runAs(owner, sql, params);
478
714
  },
715
+ wrapTransactionCallback: (fn) => sqliteTxStorage.run({ pool: this, owner, parent: sqliteTxStorage.getStore() }, fn),
479
716
  release: () => {
480
- // Single shared connection, nothing to return to a pool.
717
+ if (released)
718
+ return;
719
+ released = true;
720
+ this.releaseOwner(owner);
481
721
  },
482
722
  };
483
723
  }
@@ -487,6 +727,103 @@ class SqlitePool {
487
727
  this.closed = true;
488
728
  this.db.close();
489
729
  }
730
+ /** Is the handle inside a transaction right now? */
731
+ inTransaction() {
732
+ const reported = this.db.isTransaction;
733
+ return typeof reported === 'boolean' ? reported : this.trackedTransaction;
734
+ }
735
+ /** Run one statement for `owner`, waiting first if another caller's transaction holds the handle. */
736
+ async runAs(owner, sql, params) {
737
+ if (this.closed)
738
+ throw new errors_js_1.ConnectionError('This SQLite client has been disconnected.');
739
+ if (this.owner !== owner) {
740
+ if (this.owner !== null || this.waiters.length > 0) {
741
+ if (this.calledFromOwnersCallback())
742
+ throw statementInsideTransactionError();
743
+ await this.waitForHandle(owner);
744
+ }
745
+ else {
746
+ this.owner = owner;
747
+ }
748
+ }
749
+ try {
750
+ const result = runStatement(this.db, sql, params, this.decoderCache, this.declared);
751
+ if (TX_OPEN_HEAD.test(sql))
752
+ this.trackedTransaction = true;
753
+ else if (TX_CLOSE_HEAD.test(sql))
754
+ this.trackedTransaction = false;
755
+ return result;
756
+ }
757
+ finally {
758
+ // A statement that leaves no transaction open hands the handle on.
759
+ if (this.owner === owner && !this.inTransaction())
760
+ this.handOff();
761
+ }
762
+ }
763
+ /** True when the current async context is the callback of the transaction that owns the handle. */
764
+ calledFromOwnersCallback() {
765
+ for (let m = sqliteTxStorage.getStore(); m; m = m.parent) {
766
+ if (m.pool === this && m.owner === this.owner)
767
+ return true;
768
+ }
769
+ return false;
770
+ }
771
+ waitForHandle(owner) {
772
+ return new Promise((resolve, reject) => {
773
+ let timer;
774
+ const waiter = {
775
+ owner,
776
+ settled: false,
777
+ grant: () => {
778
+ if (timer)
779
+ clearTimeout(timer);
780
+ resolve();
781
+ },
782
+ };
783
+ this.waiters.push(waiter);
784
+ timer = setTimeout(() => {
785
+ if (waiter.settled)
786
+ return;
787
+ waiter.settled = true;
788
+ const at = this.waiters.indexOf(waiter);
789
+ if (at >= 0)
790
+ this.waiters.splice(at, 1);
791
+ reject(handleBusyError(this.busyTimeoutMs));
792
+ }, this.busyTimeoutMs);
793
+ });
794
+ }
795
+ /** Give the handle to the next waiter, which runs its statement as soon as it resumes. */
796
+ handOff() {
797
+ this.owner = null;
798
+ while (this.waiters.length > 0) {
799
+ const next = this.waiters.shift();
800
+ if (next.settled)
801
+ continue;
802
+ next.settled = true;
803
+ this.owner = next.owner;
804
+ next.grant();
805
+ return;
806
+ }
807
+ }
808
+ /**
809
+ * A checkout ended. If it still holds an open transaction (a timed-out or
810
+ * abandoned callback), roll it back first, so the next caller does not
811
+ * start inside it.
812
+ */
813
+ releaseOwner(owner) {
814
+ if (this.owner !== owner)
815
+ return;
816
+ if (this.inTransaction() && !this.closed) {
817
+ try {
818
+ this.db.exec('ROLLBACK');
819
+ }
820
+ catch {
821
+ // Nothing left to roll back.
822
+ }
823
+ this.trackedTransaction = false;
824
+ }
825
+ this.handOff();
826
+ }
490
827
  }
491
828
  exports.SqlitePool = SqlitePool;
492
829
  // ---------------------------------------------------------------------------
@@ -555,6 +892,44 @@ function sqliteColumnAffinity(type) {
555
892
  exports.sqliteDialect = {
556
893
  ...dialect_js_1.postgresDialect,
557
894
  name: 'sqlite',
895
+ capabilities: Object.freeze({
896
+ distinctOn: false,
897
+ positionalJson: false,
898
+ // NULLS FIRST / LAST since 3.30; NULL is the smallest value.
899
+ nullsOrderingClause: true,
900
+ nullsSortHigh: false,
901
+ insensitiveInList: false,
902
+ jsonDocumentEquality: 'text',
903
+ jsonExtractTyped: true,
904
+ jsonExactNumericCast: false,
905
+ numericTextComparison: false,
906
+ // Enforced only on a connection that turned `foreign_keys` on.
907
+ enforcesForeignKeys: false,
908
+ partitionLimitPushdown: false,
909
+ utcTimestampRewrite: false,
910
+ paramTypeCasts: false,
911
+ namedEnumTypes: false,
912
+ structuredValueColumns: 'scalars',
913
+ // The default SQLITE_MAX_VARIABLE_NUMBER.
914
+ maxBindParameters: 32766,
915
+ maxInsertRows: Number.POSITIVE_INFINITY,
916
+ upsertConflictTarget: 'explicit',
917
+ setClauseOrder: 'snapshot',
918
+ reselectLocksRows: false,
919
+ integerDivision: '/',
920
+ updateExpressions: true,
921
+ bulkUpsertAppliesRepeatedKeys: true,
922
+ readOnlyTransactions: false,
923
+ connectionParameters: false,
924
+ backendCancel: false,
925
+ idleTransactionTimeout: false,
926
+ abortsTransactionOnError: false,
927
+ connectionDriver: 'engine-driver',
928
+ preparesEveryStatement: false,
929
+ statementSyntax: 'sqlite',
930
+ inlineForeignKeys: true,
931
+ unboundedTextKeys: true,
932
+ }),
558
933
  resultStrategy: 'returning', // SQLite ≥ 3.35 has RETURNING
559
934
  supportsReturning: true,
560
935
  supportsILike: false,
@@ -571,6 +946,13 @@ exports.sqliteDialect = {
571
946
  // No FROM-clause LATERAL: the opt-in lateral pick plan is Postgres-only.
572
947
  supportsLateralJoin: false,
573
948
  supportsPlanCacheMode: false,
949
+ // No row locks: a write transaction locks the whole database.
950
+ rowLockModes: [],
951
+ // percentile_cont / percentile_disc exist only in builds compiled with
952
+ // SQLITE_ENABLE_PERCENTILE, which node:sqlite cannot be assumed to have.
953
+ supportsPercentileAggregates: false,
954
+ // INSERT ... ON CONFLICT (cols) DO UPDATE ... RETURNING (3.24 + 3.35).
955
+ supportsBulkUpsert: true,
574
956
  // SQLite explains a compiled query with `EXPLAIN QUERY PLAN` (four columns:
575
957
  // id, parent, notused, detail), overriding the inherited Postgres `EXPLAIN`.
576
958
  explainQuery: { prefix: 'EXPLAIN QUERY PLAN' },
@@ -634,7 +1016,7 @@ exports.sqliteDialect = {
634
1016
  }
635
1017
  // A declared-boolean column is 1/0 in storage, and the driver shim turns
636
1018
  // that back into true/false on every direct read (see
637
- // booleanResultColumns). `json_object` sees the raw integer and has no
1019
+ // resultColumnDecoders). `json_object` sees the raw integer and has no
638
1020
  // decltype to consult, so the join strategy alone would have kept handing
639
1021
  // back 1/0 while the top-level read, the batched loader and the flatten
640
1022
  // plan returned booleans, which is the same strategy-dependent type flip
@@ -646,7 +1028,8 @@ exports.sqliteDialect = {
646
1028
  // driverReportsDeclaredTypes). Without it the shim cannot name a boolean
647
1029
  // result column, so 1/0 is the answer everywhere; converting here alone
648
1030
  // would put the flip back, pointing the other way.
649
- if (t.includes('BOOL') && driverReportsDeclaredTypes()) {
1031
+ const decode = sqliteValueDecode(columnType);
1032
+ if (decode === 'boolean' && driverReportsDeclaredTypes()) {
650
1033
  return {
651
1034
  // Carried as TEXT because that is how the decode half is reached at
652
1035
  // all: the parser only runs `decode` on a string cell, so a rule that
@@ -659,6 +1042,29 @@ exports.sqliteDialect = {
659
1042
  decode: (value) => (value === '1' ? true : value === '0' ? false : value),
660
1043
  };
661
1044
  }
1045
+ // A json/jsonb column is JSON text in storage, which the shim parses on
1046
+ // every direct read (see resultColumnDecoders). `json_object` embeds the
1047
+ // same text as a JSON string, so the decode is the same parse. No cast: the
1048
+ // column reference already yields the text. Gated like the boolean rule,
1049
+ // since the direct-read half needs the same driver capability.
1050
+ if (decode === 'json' && driverReportsDeclaredTypes()) {
1051
+ return { sql: (ref) => ref, decode: parseJsonCell };
1052
+ }
1053
+ // `json_object` renders a REAL with 15 significant digits on the SQLite
1054
+ // that Node 22 and 24 bundle (3.47 to 3.51; 3.53 renders 17), so 0.1 + 0.2
1055
+ // came back 0.3 through a join and a NUMERIC past 15 digits was rounded
1056
+ // again on top of its REAL storage, while the top-level read and the
1057
+ // batched loader return the stored double. `printf('%!.17g')` prints every
1058
+ // double with enough digits to rebuild it, on every version; an INTEGER in
1059
+ // the same column is carried as its text and decoded by the safe-integer
1060
+ // policy above; text and NULL pass through as stored.
1061
+ if (/REAL|FLOA|DOUB|NUMERIC|DECIMAL|MONEY/.test(t)) {
1062
+ return {
1063
+ sql: (ref) => `CASE typeof(${ref}) WHEN 'real' THEN printf('%!.17g', ${ref}) ` +
1064
+ `WHEN 'integer' THEN CAST(${ref} AS TEXT) ELSE ${ref} END`,
1065
+ decode: decodeRealText,
1066
+ };
1067
+ }
662
1068
  // A BLOB cannot go into JSON at all: SQLite raises "JSON cannot hold BLOB
663
1069
  // values" and the whole query fails with a raw SQL logic error, where the
664
1070
  // batched loader returns the row fine. Carry hex and rebuild the bytes.
@@ -692,11 +1098,35 @@ exports.sqliteDialect = {
692
1098
  // No `= ANY(array)` in SQLite. `json_each` expands a single JSON-array param
693
1099
  // into a row set, keeping ONE placeholder (so the SQL cache stays valid
694
1100
  // regardless of list length) and handling the empty-list case correctly.
1101
+ // `x IN (SELECT ...)` compares like `x = ?`: the column's affinity and
1102
+ // collation apply to each element, as they do to a bound operand. JSON has no
1103
+ // bytes, so a binary element travels as `{"b": "<hex>"}` and is decoded back
1104
+ // to a BLOB here (see sqliteInListJson).
695
1105
  buildInClause(expr, paramRef, negated) {
696
- return `${expr} ${negated ? 'NOT IN' : 'IN'} (SELECT value FROM json_each(${paramRef}))`;
1106
+ return (`${expr} ${negated ? 'NOT IN' : 'IN'} ` +
1107
+ `(SELECT CASE WHEN type = 'object' THEN unhex(json_extract(value, '$.b')) ELSE value END ` +
1108
+ `FROM json_each(${paramRef}))`);
697
1109
  },
698
1110
  inClauseParam(values) {
699
- return JSON.stringify(values ?? []);
1111
+ return sqliteInListJson(Array.isArray(values) ? values : []);
1112
+ },
1113
+ /**
1114
+ * A string written to a numeric or boolean column. SQLite's type affinity
1115
+ * keeps text it cannot convert as TEXT in that column, where PostgreSQL
1116
+ * refuses it at the server: `'abc'` in an INTEGER column, or `'true'` in a
1117
+ * boolean one (stored 1/0), which `{ flag: true }` then never matches. A
1118
+ * string that is a number (`'12.5'`, `'9007199254740993'`, `' 7 '`) is
1119
+ * accepted and converted by SQLite, as before.
1120
+ */
1121
+ checkTextWrite(table, column, columnType, value) {
1122
+ if (!SQLITE_NUMERIC_COLUMN_TYPE.test(columnType.trim()))
1123
+ return undefined;
1124
+ if (SQLITE_NUMERIC_LITERAL.test(value))
1125
+ return undefined;
1126
+ const boolean = /BOOL/i.test(columnType);
1127
+ return (`The string written to "${column}" on "${table}", a column of type ${columnType}, is not a number, so SQLite ` +
1128
+ `would store it as text in that column; PostgreSQL refuses it. ` +
1129
+ (boolean ? 'Write true or false.' : 'Write a number, or a string holding one, such as "12.5".'));
700
1130
  },
701
1131
  buildReturningClause(selection = '*') {
702
1132
  return ` RETURNING ${selection === '*' ? '*' : selection.join(', ')}`;
@@ -719,6 +1149,21 @@ exports.sqliteDialect = {
719
1149
  params: input.rowValues.flat(),
720
1150
  };
721
1151
  },
1152
+ buildBulkUpsertStatement(input) {
1153
+ // VALUES, not `INSERT ... SELECT`: after a SELECT, SQLite cannot tell a
1154
+ // trailing `ON CONFLICT` from the SELECT's own clauses. The rest is the
1155
+ // PostgreSQL grammar (3.24 upsert, 3.35 RETURNING), `excluded` included.
1156
+ let n = 0;
1157
+ const tuples = input.rowValues.map((row) => `(${row.map(() => this.paramPlaceholder(++n)).join(', ')})`).join(', ');
1158
+ const params = input.rowValues.flat();
1159
+ return {
1160
+ sql: `INSERT INTO ${input.table} (${input.columns.join(', ')}) VALUES ${tuples} ` +
1161
+ `ON CONFLICT (${input.conflictColumns.join(', ')}) ` +
1162
+ (0, dialect_js_1.onConflictAction)(input, params, { target: input.table, incoming: 'excluded' }) +
1163
+ this.buildReturningClause(input.returning),
1164
+ params,
1165
+ };
1166
+ },
722
1167
  buildDefaultValuesInsertStatement(input) {
723
1168
  // SQLite's `DEFAULT VALUES` inserts exactly one row and the engine accepts
724
1169
  // neither `VALUES (DEFAULT, …)` nor MySQL's empty `VALUES ()` tuple, so
@@ -739,6 +1184,12 @@ exports.sqliteDialect = {
739
1184
  (input.updateWhere ? ` WHERE ${input.updateWhere}` : '') +
740
1185
  this.buildReturningClause(input.returning));
741
1186
  },
1187
+ // A SQLite transaction is always SERIALIZABLE, which satisfies every level
1188
+ // `$transaction({ isolationLevel })` can ask for, and the inherited
1189
+ // `BEGIN ISOLATION LEVEL ...` is a syntax error here.
1190
+ beginStatement() {
1191
+ return 'BEGIN';
1192
+ },
742
1193
  buildInsensitiveLike(column, paramRef) {
743
1194
  // COLLATE NOCASE is ASCII-only (no Unicode case folding), documented limit.
744
1195
  return `${column} LIKE ${paramRef} COLLATE NOCASE`;
@@ -767,6 +1218,11 @@ exports.sqliteDialect = {
767
1218
  buildColumnType(input) {
768
1219
  return sqliteColumnAffinity(input.type);
769
1220
  },
1221
+ /** SQLite has no enum type: a TEXT column, and a CHECK that admits only the labels (compared exactly, BINARY collation). */
1222
+ buildEnumColumn(column, labels) {
1223
+ const literals = labels.map((label) => `'${label.replace(/'/g, "''")}'`);
1224
+ return { type: 'TEXT', check: `${column} IN (${literals.join(', ')})` };
1225
+ },
770
1226
  buildColumnDefinition(input) {
771
1227
  const isSerial = /serial/i.test(input.type);
772
1228
  const parts = [input.name];
@@ -942,16 +1398,19 @@ function introspectSqliteDatabase(db, options = {}) {
942
1398
  // same logical schema, legacy-first naming, per-column disambiguation,
943
1399
  // collision resolution against scalar column fields, and the conservative
944
1400
  // pure-junction manyToMany auto-detection included.
945
- const relationsByTable = (0, introspect_js_1.deriveEngineRelations)(tableNames, foreignKeys, pkByTable, columnsByTable);
1401
+ // One field per column, as the PostgreSQL introspector requires.
1402
+ for (const [tableName, cols] of columnsByTable)
1403
+ (0, introspect_js_1.assertDistinctColumnFields)(tableName, cols);
1404
+ const relationsByTable = (0, introspect_js_1.deriveEngineRelations)(tableNames, foreignKeys, pkByTable, columnsByTable, options.logger);
946
1405
  // ----- Assemble TableMetadata -----
947
- const tables = {};
1406
+ const tables = (0, introspect_js_1.nullRecord)();
948
1407
  for (const tableName of tableNames) {
949
1408
  const columns = columnsByTable.get(tableName) ?? [];
950
- const columnMap = {};
951
- const reverseColumnMap = {};
1409
+ const columnMap = (0, introspect_js_1.nullRecord)();
1410
+ const reverseColumnMap = (0, introspect_js_1.nullRecord)();
952
1411
  const dateColumns = new Set();
953
- const dialectTypes = {};
954
- const pgTypes = {};
1412
+ const dialectTypes = (0, introspect_js_1.nullRecord)();
1413
+ const pgTypes = (0, introspect_js_1.nullRecord)();
955
1414
  const allColumns = [];
956
1415
  for (const col of columns) {
957
1416
  columnMap[col.field] = col.name;
@@ -973,7 +1432,7 @@ function introspectSqliteDatabase(db, options = {}) {
973
1432
  allColumns,
974
1433
  primaryKey: pkByTable.get(tableName) ?? [],
975
1434
  uniqueColumns: uniqueByTable.get(tableName) ?? [],
976
- relations: relationsByTable.get(tableName) ?? {},
1435
+ relations: relationsByTable.get(tableName) ?? (0, introspect_js_1.nullRecord)(),
977
1436
  indexes: indexesByTable.get(tableName) ?? [],
978
1437
  };
979
1438
  }
@@ -998,7 +1457,7 @@ async function introspectSqlite(options) {
998
1457
  throw new errors_js_1.ConnectionError(`Failed to open SQLite database "${options.connectionString}": ${err.message}`);
999
1458
  }
1000
1459
  try {
1001
- return introspectSqliteDatabase(db, { include: options.include, exclude: options.exclude });
1460
+ return introspectSqliteDatabase(db, { include: options.include, exclude: options.exclude, logger: options.logger });
1002
1461
  }
1003
1462
  finally {
1004
1463
  db.close();
@@ -1013,9 +1472,17 @@ function openSqliteDatabase(target, options) {
1013
1472
  catch (err) {
1014
1473
  throw new errors_js_1.ConnectionError(`Failed to open SQLite database "${target}": ${err.message}`);
1015
1474
  }
1016
- db.exec(`PRAGMA busy_timeout = ${Number(options.busyTimeoutMs ?? 5000)}`);
1017
- if (options.foreignKeys !== false)
1018
- db.exec('PRAGMA foreign_keys = ON');
1475
+ try {
1476
+ db.exec(`PRAGMA busy_timeout = ${Number(options.busyTimeoutMs ?? 5000)}`);
1477
+ if (options.foreignKeys !== false)
1478
+ db.exec('PRAGMA foreign_keys = ON');
1479
+ }
1480
+ catch (err) {
1481
+ // Opened here and about to be unreachable: close it rather than leave the
1482
+ // file handle to the garbage collector.
1483
+ db.close();
1484
+ throw err;
1485
+ }
1019
1486
  if (target !== ':memory:' && options.wal !== false) {
1020
1487
  try {
1021
1488
  db.exec('PRAGMA journal_mode = WAL');
@@ -1046,16 +1513,47 @@ function openSqliteDatabase(target, options) {
1046
1513
  * ```
1047
1514
  */
1048
1515
  function turbineSqlite(target, schema, options = {}) {
1049
- const db = typeof target === 'string' ? openSqliteDatabase(target, options) : target;
1050
- const pool = new SqlitePool(db);
1051
- // Everything that is not a SQLite pragma is client config and rides through
1052
- // untouched, so a new TurbineConfig option works here the day it lands. The
1053
- // engine-owned keys come last, so no caller can unbind the dialect.
1054
- const { wal: _wal, busyTimeoutMs: _busyTimeoutMs, foreignKeys: _foreignKeys, ...clientConfig } = options;
1055
- return new client_js_1.TurbineClient({
1056
- ...clientConfig,
1057
- pool,
1058
- dialect: exports.sqliteDialect,
1059
- preparedStatements: false,
1060
- }, schema);
1516
+ const owns = typeof target === 'string';
1517
+ const db = owns ? openSqliteDatabase(target, options) : target;
1518
+ let pool;
1519
+ let client;
1520
+ try {
1521
+ pool = new SqlitePool(db, { busyTimeoutMs: options.busyTimeoutMs, schema });
1522
+ // Everything that is not a SQLite pragma is client config and rides through
1523
+ // untouched, so a new TurbineConfig option works here the day it lands. The
1524
+ // engine-owned keys come last, so no caller can unbind the dialect.
1525
+ const { wal: _wal, busyTimeoutMs: _busyTimeoutMs, foreignKeys: _foreignKeys, ...clientConfig } = options;
1526
+ client = new client_js_1.TurbineClient({
1527
+ ...clientConfig,
1528
+ pool,
1529
+ dialect: exports.sqliteDialect,
1530
+ preparedStatements: false,
1531
+ }, schema);
1532
+ }
1533
+ catch (err) {
1534
+ // A failed factory hands the caller nothing to close, so a handle it
1535
+ // OPENED is closed here (the client constructor refuses a bad config, a
1536
+ // static global filter naming no table or column for one). A handle the
1537
+ // caller passed in is the caller's and stays open.
1538
+ if (owns)
1539
+ db.close();
1540
+ throw err;
1541
+ }
1542
+ if (owns) {
1543
+ // Turbine opened this handle, so disconnect()/end() must close it, as the
1544
+ // other engine factories close a pool they built: the client sees
1545
+ // `TurbineConfig.pool` as external and would otherwise leave it open, still
1546
+ // answering queries after disconnect(). A handle the caller passed in stays
1547
+ // the caller's (disconnect() is a no-op for it).
1548
+ const baseDisconnect = client.disconnect.bind(client);
1549
+ const close = async (options) => {
1550
+ const result = options === undefined ? await baseDisconnect() : await baseDisconnect(options);
1551
+ await pool.end();
1552
+ return result ?? undefined;
1553
+ };
1554
+ const patch = client;
1555
+ patch.disconnect = close;
1556
+ patch.end = close;
1557
+ }
1558
+ return client;
1061
1559
  }