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
package/dist/sqlite.js CHANGED
@@ -19,18 +19,24 @@
19
19
  * - `RETURNING` + `ON CONFLICT … DO UPDATE` (SQLite ≥ 3.35) → create / upsert /
20
20
  * update / delete return real rows in a single statement (`resultStrategy =
21
21
  * 'returning'`, same as Postgres).
22
- * - **Single-writer:** one connection / one write transaction at a time;
23
- * concurrent writers get `SQLITE_BUSY` (treated as retryable). `journal_mode =
24
- * WAL` is enabled for file databases to allow concurrent readers.
22
+ * - **Single-writer:** one connection / one transaction at a time. On one
23
+ * client, statements from other callers wait (FIFO, up to `busyTimeoutMs`)
24
+ * while a transaction is open instead of running inside it; another
25
+ * process's writer gets `SQLITE_BUSY`. Both surface as the retryable
26
+ * `SerializationFailureError`. `journal_mode = WAL` is enabled for file
27
+ * databases to allow concurrent readers across connections.
25
28
  * - **Unsupported (throw `UnsupportedFeatureError`):** pgvector distance ops,
26
29
  * LISTEN/NOTIFY (`$listen` / `$notify`), RLS `sessionContext`. Advisory-lock
27
30
  * migration locking is unavailable, SQLite is single-writer, so migrations
28
31
  * serialize naturally.
29
- * - **Type affinity caveats:** SQLite has no native `BOOLEAN` (0/1 integers) or
30
- * `DATE` (TEXT/INTEGER). Booleans bind as 1/0; `Date` values bind as ISO-8601
31
- * text; columns declared `TIMESTAMP`/`DATETIME`/`DATE` are coerced back to
32
- * `Date`. Integers wider than `Number.MAX_SAFE_INTEGER` come back as strings
33
- * (the same safe-int policy Turbine uses for Postgres `int8`).
32
+ * - **Type affinity caveats:** SQLite has no native `BOOLEAN` (0/1 integers),
33
+ * `JSON` (text) or `DATE` (TEXT/INTEGER). Booleans bind as 1/0; `Date` values
34
+ * bind as ISO-8601 text; an integral JS number binds as INTEGER. A column the
35
+ * schema declares boolean or json/jsonb reads back as a boolean or a parsed
36
+ * value on every path; date/time columns are coerced back to `Date`.
37
+ * Integers wider than `Number.MAX_SAFE_INTEGER` come back as strings (the
38
+ * same safe-int policy Turbine uses for Postgres `int8`). `numeric` has no
39
+ * exact storage: it reads back as a JS number.
34
40
  * - **Case-insensitive matching** uses `COLLATE NOCASE`, which is **ASCII-only**
35
41
  * (no Unicode case folding).
36
42
  *
@@ -68,7 +74,7 @@
68
74
  *
69
75
  * const db = turbineSqlite(':memory:', schemaDefToMetadata(schema));
70
76
  * for (const stmt of schemaToSQL(schema, { dialect: sqliteDialect })) {
71
- * await db.raw([stmt] as never);
77
+ * await db.rawUnsafe(stmt);
72
78
  * }
73
79
  *
74
80
  * // `db.table(...)`, not `db.users`: the typed property accessors are emitted
@@ -77,13 +83,16 @@
77
83
  * await db.disconnect();
78
84
  * ```
79
85
  */
86
+ import { AsyncLocalStorage } from 'node:async_hooks';
80
87
  import { createRequire } from 'node:module';
81
- import { TurbineClient } from './client.js';
82
- import { postgresDialect, } from './dialect.js';
83
- import { ConnectionError, UnsupportedFeatureError, ValidationError } from './errors.js';
84
- import { applyTableFilters, deriveEngineRelations } from './introspect.js';
88
+ import { TurbineClient, } from './client.js';
89
+ import { onConflictAction, postgresDialect, } from './dialect.js';
90
+ import { ConnectionError, SerializationFailureError, UnsupportedFeatureError, ValidationError } from './errors.js';
91
+ import { applyTableFilters, assertDistinctColumnFields, deriveEngineRelations, nullRecord, printableCatalogName, } from './introspect.js';
92
+ import { withoutTrailingComment } from './query/sql-comment.js';
85
93
  import { LRUCache } from './query/utils.js';
86
94
  import { isDateType, snakeToCamel, } from './schema.js';
95
+ import { rewritePositionalPlaceholders } from './sql-placeholders.js';
87
96
  let cachedDatabaseSync;
88
97
  /**
89
98
  * Lazily load `node:sqlite`'s `DatabaseSync` constructor.
@@ -121,7 +130,8 @@ function loadDatabaseSync() {
121
130
  * Coerce an arbitrary JS value into something `node:sqlite` can bind.
122
131
  *
123
132
  * `node:sqlite` throws on `boolean`, `undefined`, `Date`, and plain objects, so
124
- * we normalize: booleans → 1/0, `undefined`/`null` → NULL, `Date` → ISO text,
133
+ * we normalize: booleans → 1/0, a safe integer → INTEGER (see below),
134
+ * `undefined`/`null` → NULL, `Date` → ISO text,
125
135
  * `Uint8Array`/`Buffer` → BLOB, and any remaining object/array → JSON text
126
136
  * (matches how Turbine already pre-serializes JSON filter params).
127
137
  */
@@ -132,6 +142,12 @@ function toSqliteParam(value) {
132
142
  case 'boolean':
133
143
  return value ? 1 : 0;
134
144
  case 'number':
145
+ // `node:sqlite` binds every JS number as REAL, so `views / 2` divided as
146
+ // floats (75 / 2 = 37.5, stored in an INTEGER column) and an integral
147
+ // number written to a TEXT column was stored as '3.0'. Binding a safe
148
+ // integer as an INTEGER gives the arithmetic and text PostgreSQL gives.
149
+ // -0 stays REAL: as an integer it would lose its sign.
150
+ return Number.isSafeInteger(value) && !Object.is(value, -0) ? BigInt(value) : value;
135
151
  case 'bigint':
136
152
  case 'string':
137
153
  return value;
@@ -142,6 +158,44 @@ function toSqliteParam(value) {
142
158
  return value; // also covers Buffer
143
159
  return JSON.stringify(value);
144
160
  }
161
+ /**
162
+ * The JSON text an `in` / `notIn` list binds as, element by element in the
163
+ * form {@link toSqliteParam} would bind it, so a list element compares exactly
164
+ * as the same value given to `equals` does. A bigint is written as a JSON
165
+ * integer (exact to 64 bits; `JSON.stringify` refuses one), an infinity as the
166
+ * overflowing literal SQLite's JSON parser reads as one, and a binary value as
167
+ * `{"b": "<hex>"}`, which {@link sqliteDialect.buildInClause} turns back into a
168
+ * BLOB. Any other object binds as its JSON text, as it would on its own.
169
+ */
170
+ function sqliteInListJson(values) {
171
+ const parts = values.map((value) => {
172
+ if (value === undefined || value === null)
173
+ return 'null';
174
+ switch (typeof value) {
175
+ case 'boolean':
176
+ return value ? '1' : '0';
177
+ case 'number':
178
+ if (Number.isFinite(value))
179
+ return JSON.stringify(value);
180
+ if (Number.isNaN(value))
181
+ return 'null';
182
+ return value > 0 ? '9e999' : '-9e999';
183
+ case 'bigint':
184
+ return value.toString();
185
+ case 'string':
186
+ return JSON.stringify(value);
187
+ }
188
+ if (value instanceof Uint8Array) {
189
+ let hex = '';
190
+ for (const byte of value)
191
+ hex += byte.toString(16).padStart(2, '0');
192
+ return `{"b":"${hex}"}`;
193
+ }
194
+ const bound = toSqliteParam(value);
195
+ return JSON.stringify(bound);
196
+ });
197
+ return `[${parts.join(',')}]`;
198
+ }
145
199
  /**
146
200
  * Normalize a single column value read from SQLite. With `setReadBigInts(true)`
147
201
  * every integer column comes back as a `bigint`; apply the same safe-integer
@@ -156,44 +210,142 @@ function normalizeValue(value) {
156
210
  }
157
211
  return value;
158
212
  }
213
+ /**
214
+ * The decode a column of `type` needs on SQLite, or `undefined`. `type` is a
215
+ * declared SQLite type or a schema type (`bool`, `jsonb`). The ONE answer both
216
+ * read paths use: the driver shim for direct reads and {@link sqliteDialect}'s
217
+ * `jsonWireRule` for `with` rows, so the two cannot disagree about a column.
218
+ */
219
+ function sqliteValueDecode(type) {
220
+ const t = type.toUpperCase();
221
+ if (t.includes('BOOL'))
222
+ return 'boolean';
223
+ if (t === 'JSON' || t === 'JSONB')
224
+ return 'json';
225
+ return undefined;
226
+ }
227
+ function declaredDecoders(schema) {
228
+ if (!schema)
229
+ return null;
230
+ let out = null;
231
+ for (const [table, meta] of Object.entries(schema.tables)) {
232
+ for (const column of meta.columns) {
233
+ const type = column.pgType ?? meta.pgTypes?.[column.name];
234
+ const decode = typeof type === 'string' ? sqliteValueDecode(type) : undefined;
235
+ if (decode === undefined)
236
+ continue;
237
+ if (out === null)
238
+ out = new Map();
239
+ out.set(`${table}\u0000${column.name}`, decode);
240
+ }
241
+ }
242
+ return out;
243
+ }
244
+ /**
245
+ * A decimal integer or float as SQLite prints it (`printf('%!.17g')` / `CAST(... AS TEXT)`).
246
+ *
247
+ * Linear by construction: the fraction's digits can only follow a literal `.`,
248
+ * so a run of digits has exactly one way to match. The earlier `\d+\.?\d*`
249
+ * could split a dot-less run between its two quantifiers in every possible
250
+ * place, which made a long non-numeric cell take seconds to reject (quadratic:
251
+ * 40,000 digits and an `x` took 0.8 s, a million take 1.5 ms now).
252
+ */
253
+ const SQLITE_NUMBER_TEXT = /^-?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
254
+ /**
255
+ * The text SQLite's INTEGER, REAL and NUMERIC affinities convert to a number,
256
+ * measured against SQLite 3.51: an optional sign, a decimal integer or float,
257
+ * an optional exponent, and ASCII whitespace around it. Anything else
258
+ * (`'abc'`, `''`, `'Infinity'`, `'0x10'`, `'1_000'`) is stored as TEXT in the
259
+ * numeric column. Linear for the same reason as {@link SQLITE_NUMBER_TEXT}.
260
+ */
261
+ const SQLITE_NUMERIC_LITERAL = /^[ \t\n\v\f\r]*[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?[ \t\n\v\f\r]*$/i;
262
+ /**
263
+ * Declared column types whose SQLite affinity is numeric (INTEGER, REAL or
264
+ * NUMERIC), boolean included (stored 1/0). Matched whole, not by SQLite's own
265
+ * "contains INT" rule: the type a schema records for a column can be an enum's
266
+ * name (`point_kind`), which that rule reads as INTEGER while the column is
267
+ * TEXT. A type this misses is written as SQLite always wrote it, so a miss
268
+ * costs a refusal, never a wrong one.
269
+ */
270
+ 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;
271
+ /**
272
+ * Decode a REAL / NUMERIC cell carried as text through a `with` join back to
273
+ * the value a direct read returns: an integer by the safe-integer policy, a
274
+ * double as the number its 17 digits name, `Inf` / `-Inf` as infinities.
275
+ */
276
+ function decodeRealText(value) {
277
+ if (typeof value !== 'string')
278
+ return value;
279
+ if (/^-?\d+$/.test(value)) {
280
+ const asNumber = Number(value);
281
+ return Number.isSafeInteger(asNumber) ? asNumber : value;
282
+ }
283
+ if (value === 'Inf')
284
+ return Number.POSITIVE_INFINITY;
285
+ if (value === '-Inf')
286
+ return Number.NEGATIVE_INFINITY;
287
+ return SQLITE_NUMBER_TEXT.test(value) ? Number(value) : value;
288
+ }
289
+ /** Read a JSON column's stored text as its value; text that is not JSON is returned as stored. */
290
+ function parseJsonCell(value) {
291
+ if (typeof value !== 'string')
292
+ return value;
293
+ try {
294
+ return JSON.parse(value);
295
+ }
296
+ catch {
297
+ return value;
298
+ }
299
+ }
159
300
  /**
160
301
  * Convert a `node:sqlite` null-prototype row into a normalized plain object.
161
302
  *
162
- * `booleanKeys`, when present, names the result columns whose ORIGIN column is
163
- * declared with a boolean affinity; their 1/0 storage is turned back into
164
- * `true`/`false` (see {@link booleanResultColumns}). Anything else in such a
165
- * column, and every column not listed, passes through untouched.
303
+ * `decoders`, when present, names the result columns whose ORIGIN column is a
304
+ * boolean or a JSON column (see {@link resultColumnDecoders}): 1/0 becomes
305
+ * `true`/`false`, JSON text becomes its value. Anything else in such a column,
306
+ * and every column not listed, passes through untouched.
166
307
  */
167
- function normalizeRow(row, booleanKeys) {
308
+ function normalizeRow(row, decoders) {
168
309
  const out = {};
169
310
  for (const key of Object.keys(row)) {
170
311
  const value = normalizeValue(row[key]);
171
- out[key] = booleanKeys?.has(key) && (value === 1 || value === 0) ? value === 1 : value;
312
+ const decode = decoders?.get(key);
313
+ out[key] =
314
+ decode === undefined
315
+ ? value
316
+ : decode === 'json'
317
+ ? parseJsonCell(value)
318
+ : value === 1 || value === 0
319
+ ? value === 1
320
+ : value;
172
321
  }
173
322
  return out;
174
323
  }
175
324
  /**
176
- * The result columns of a prepared statement whose ORIGIN column is declared
177
- * boolean, or `null` when there are none (the overwhelmingly common case).
325
+ * The result columns of a prepared statement that need decoding, or `null`
326
+ * when there are none (the overwhelmingly common case).
178
327
  *
179
- * WHY THIS EXISTS. SQLite has no boolean storage class: a `BOOLEAN` column
180
- * holds 1/0 integers. Turbine still generates `ok: boolean` for it
181
- * ({@link sqliteTypeToTs} maps `/bool/` to `boolean`), so `row.ok === true` was
182
- * false, `JSON.stringify(row)` emitted `1`, and a value the ORM itself wrote as
183
- * `true` came back as a number. The declared type is the ONLY thing that says
184
- * "this integer is a boolean", and it lives in the schema, not in the value.
328
+ * WHY THIS EXISTS. SQLite has no boolean or JSON storage class: a boolean is
329
+ * stored as 1/0 and a JSON document as text. Turbine still generates
330
+ * `ok: boolean` and a JSON value for those columns, and PostgreSQL returns
331
+ * exactly that, so a value the ORM itself wrote as `true` or `{ a: 1 }` came
332
+ * back as `1` or `'{"a":1}'`.
185
333
  *
186
- * The decltype comes from the driver rather than from Turbine's metadata
187
- * because the driver knows which TABLE each result column came from.
188
- * `StatementSync.columns()` reports `type` as `sqlite3_column_decltype`, which
189
- * is non-null only for a direct table-column reference and null for any
190
- * expression, so a computed column can never be coerced by accident.
334
+ * Two sources name such a column, both read from the driver's per-column
335
+ * description, because only the driver knows which TABLE a result column came
336
+ * from. `StatementSync.columns()` reports the declared type
337
+ * (`sqlite3_column_decltype`) and the origin table and column, all of which are
338
+ * null for an expression, so a computed column is never decoded by accident.
339
+ * - A declared type naming a boolean (`BOOLEAN`), as before.
340
+ * - The client's schema: an origin column declared boolean or json/jsonb there.
341
+ * Turbine's own DDL stores those as INTEGER and TEXT, so the declared type
342
+ * alone missed every table Turbine created.
191
343
  *
192
- * Node gained `columns()` in 22.13 / 23.4 while this engine's floor is 22.5, so
193
- * it is feature-detected: on the narrow band without it, booleans keep reading
194
- * back as 1/0 exactly as before rather than the engine refusing to run.
344
+ * Node gained `columns()` in 22.16 / 23.11 while this engine's floor is 22.5, so
345
+ * it is feature-detected: on the narrow band without it, these columns keep
346
+ * reading back as stored rather than the engine refusing to run.
195
347
  */
196
- function booleanResultColumns(stmt) {
348
+ function resultColumnDecoders(stmt, declared) {
197
349
  const columnsFn = stmt.columns;
198
350
  if (typeof columnsFn !== 'function')
199
351
  return null;
@@ -203,18 +355,24 @@ function booleanResultColumns(stmt) {
203
355
  }
204
356
  catch {
205
357
  // A statement the driver cannot describe (some PRAGMA forms) is not worth
206
- // failing a query over; skip the coercion for it.
358
+ // failing a query over; skip the decoding for it.
207
359
  return null;
208
360
  }
209
361
  let out = null;
210
362
  for (const c of described) {
211
- if (typeof c.name !== 'string' || typeof c.type !== 'string')
363
+ if (typeof c.name !== 'string')
212
364
  continue;
213
- if (!/bool/i.test(c.type))
365
+ let decode;
366
+ if (typeof c.type === 'string' && sqliteValueDecode(c.type) === 'boolean')
367
+ decode = 'boolean';
368
+ else if (declared !== null && typeof c.table === 'string' && typeof c.column === 'string') {
369
+ decode = declared.get(`${c.table}\u0000${c.column}`);
370
+ }
371
+ if (decode === undefined)
214
372
  continue;
215
373
  if (out === null)
216
- out = new Set();
217
- out.add(c.name);
374
+ out = new Map();
375
+ out.set(c.name, decode);
218
376
  }
219
377
  return out;
220
378
  }
@@ -228,11 +386,11 @@ let cachedSqliteNamespace;
228
386
  /**
229
387
  * Can the driver report a result column's DECLARED type?
230
388
  *
231
- * This is the same capability {@link booleanResultColumns} feature-detects, and
389
+ * This is the same capability {@link resultColumnDecoders} feature-detects, and
232
390
  * the two have to be asked the SAME question or they split: that helper is what
233
391
  * turns 1/0 into `true`/`false` on every direct read, and the boolean
234
392
  * `jsonWireRule` is what does it on the join path. On the Node band with
235
- * `node:sqlite` but without `StatementSync.columns()` (>= 22.5, < 22.13 / 23.4)
393
+ * `node:sqlite` but without `StatementSync.columns()` (>= 22.5, < 22.16 / 23.11)
236
394
  * the helper returns `null` and every direct read keeps 1/0, so a join that
237
395
  * still converted would produce exactly the strategy-dependent type flip the
238
396
  * rule exists to prevent, on the one runtime where nothing else can catch it.
@@ -262,7 +420,12 @@ const ROW_RETURNING_HEAD = /^\s*(?:select|with|pragma|explain|values)\b/i;
262
420
  * A `RETURNING` clause turns any write into a row-producing statement.
263
421
  */
264
422
  function statementReturnsRows(sql) {
265
- return ROW_RETURNING_HEAD.test(sql) || /\breturning\b/i.test(sql);
423
+ // A `sqlComment` (query/sql-comment.ts) sits after the statement and its
424
+ // values are data, so the RETURNING test reads the statement without it: a
425
+ // value such as `orders-returning` must not turn a write into a row read
426
+ // and its change count into 0.
427
+ const code = withoutTrailingComment(sql);
428
+ return ROW_RETURNING_HEAD.test(code) || /\breturning\b/i.test(code);
266
429
  }
267
430
  /**
268
431
  * Augment a `node:sqlite` error with the Postgres-shaped fields that
@@ -334,18 +497,34 @@ function augmentSqliteError(err) {
334
497
  target.code = '23514';
335
498
  return err;
336
499
  default:
337
- // SQLITE_BUSY (5) / SQLITE_LOCKED (6) / SQLITE_BUSY_SNAPSHOT (261) etc.
338
- if (primary === 5 || primary === 6) {
339
- // Map to serialization_failure so withRetry()/$retry() retry it.
340
- target.code = '40001';
341
- return err;
342
- }
500
+ // SQLITE_BUSY (5) / SQLITE_LOCKED (6) and their extended codes
501
+ // (SQLITE_BUSY_SNAPSHOT 517, SQLITE_LOCKED_SHAREDCACHE 262, ...). Built as
502
+ // the typed retryable error HERE rather than annotated, because the raw
503
+ // paths (`db.transaction(client => ...)`, `db.pool.query`) never pass
504
+ // through `wrapPgError` and would otherwise hand back the driver error.
505
+ if (primary === 5 || primary === 6)
506
+ return sqliteBusyError(e.errcode, message, err);
343
507
  // SQLITE_ERROR (1): the statement itself is wrong. See the note above.
344
508
  if (primary === 1)
345
509
  return sqliteLogicError(message, err);
346
510
  return err;
347
511
  }
348
512
  }
513
+ /**
514
+ * A `SQLITE_BUSY` / `SQLITE_LOCKED` failure as the retryable typed error, so
515
+ * `withRetry` / `$retry` and `err.isRetryable` handle it on every path.
516
+ * `busy_timeout` does not apply to a deferred transaction that read first and
517
+ * then tried to write (SQLITE_BUSY_SNAPSHOT): only retrying the WHOLE
518
+ * transaction can succeed, which is what the hint says.
519
+ */
520
+ function sqliteBusyError(errcode, message, cause) {
521
+ const kind = (errcode & 0xff) === 5 ? 'SQLITE_BUSY' : 'SQLITE_LOCKED';
522
+ return new SerializationFailureError({
523
+ message: `SQLite database is busy (${kind}, code ${errcode}): ${message}. Another connection holds the lock it needs. ` +
524
+ 'Retry the whole transaction (withRetry does), or raise busyTimeoutMs.',
525
+ cause,
526
+ });
527
+ }
349
528
  /**
350
529
  * The `ValidationError` for a `SQLITE_ERROR`, with a hint for the two spellings
351
530
  * that account for nearly all of them.
@@ -363,17 +542,17 @@ function augmentSqliteError(err) {
363
542
  function sqliteLogicError(message, cause) {
364
543
  const missingTable = /^no such table:\s*(\S+)/i.exec(message);
365
544
  if (missingTable) {
366
- return new ValidationError(`SQLite has no table "${missingTable[1]}": ${message}. ` +
545
+ return new ValidationError(`SQLite has no table "${printableCatalogName(missingTable[1])}": ${printableCatalogName(message)}. ` +
367
546
  'Create it first (run your migrations, or execute the CREATE TABLE statements for this schema); ' +
368
547
  'an in-memory database starts empty on every connection.', { cause });
369
548
  }
370
549
  const missingColumn = /^no such column:\s*(\S+)/i.exec(message);
371
550
  if (missingColumn) {
372
- return new ValidationError(`SQLite has no column "${missingColumn[1]}": ${message}. ` +
551
+ return new ValidationError(`SQLite has no column "${printableCatalogName(missingColumn[1])}": ${printableCatalogName(message)}. ` +
373
552
  'Check the spelling against the table as it exists in this database, ' +
374
553
  'and re-run `turbine generate` if the schema has changed.', { cause });
375
554
  }
376
- return new ValidationError(`SQLite rejected the statement: ${message}`, { cause });
555
+ return new ValidationError(`SQLite rejected the statement: ${printableCatalogName(message)}`, { cause });
377
556
  }
378
557
  function normalizeQueryArgs(arg, values) {
379
558
  if (typeof arg === 'string')
@@ -403,8 +582,15 @@ function toNamedBinding(values) {
403
582
  }
404
583
  return named;
405
584
  }
406
- function runStatement(db, sql, values, booleanCache) {
585
+ /** The refusal for a placeholder the shim cannot bind (see rewritePositionalPlaceholders). */
586
+ function refusePlaceholder(message) {
587
+ throw new ValidationError(message);
588
+ }
589
+ function runStatement(db, text, values, decoderCache, declared = null) {
407
590
  const binding = toNamedBinding(values);
591
+ // `$n` is the spelling `rawUnsafe` documents for every engine; SQLite would
592
+ // read `$1` as a parameter NAMED "1", which the `{ p1, ... }` binding lacks.
593
+ const sql = rewritePositionalPlaceholders(text, 'sqlite', values.length, (i) => `:p${i}`, refusePlaceholder);
408
594
  let stmt;
409
595
  try {
410
596
  stmt = db.prepare(sql);
@@ -415,15 +601,15 @@ function runStatement(db, sql, values, booleanCache) {
415
601
  stmt.setReadBigInts(true);
416
602
  try {
417
603
  if (statementReturnsRows(sql)) {
418
- // Describe each distinct statement ONCE. `null` (no boolean column) is a
604
+ // Describe each distinct statement ONCE. `null` (nothing to decode) is a
419
605
  // cached answer too, so the ordinary query pays a single Map hit per call
420
606
  // and nothing per row.
421
- let booleanKeys = booleanCache?.get(sql);
422
- if (booleanKeys === undefined) {
423
- booleanKeys = booleanResultColumns(stmt);
424
- booleanCache?.set(sql, booleanKeys);
607
+ let decoders = decoderCache?.get(sql);
608
+ if (decoders === undefined) {
609
+ decoders = resultColumnDecoders(stmt, declared);
610
+ decoderCache?.set(sql, decoders);
425
611
  }
426
- const rows = (binding ? stmt.all(binding) : stmt.all()).map((r) => normalizeRow(r, booleanKeys ?? undefined));
612
+ const rows = (binding ? stmt.all(binding) : stmt.all()).map((r) => normalizeRow(r, decoders ?? undefined));
427
613
  return { rows, rowCount: rows.length };
428
614
  }
429
615
  const info = binding ? stmt.run(binding) : stmt.run();
@@ -435,42 +621,96 @@ function runStatement(db, sql, values, booleanCache) {
435
621
  throw augmentSqliteError(err);
436
622
  }
437
623
  }
624
+ /** Default wait, in ms, for a transaction another caller holds open on the handle. */
625
+ const DEFAULT_BUSY_TIMEOUT_MS = 5000;
626
+ const sqliteTxStorage = new AsyncLocalStorage();
627
+ /** Statement heads that leave the handle inside a transaction when they succeed. */
628
+ const TX_OPEN_HEAD = /^\s*(?:begin|savepoint)\b/i;
629
+ /** Statement heads that end the transaction (not `ROLLBACK TO`, which keeps it open). */
630
+ const TX_CLOSE_HEAD = /^\s*(?:commit|end|rollback(?!\s+(?:transaction\s+)?to\b))\b/i;
631
+ /**
632
+ * The one statement the pool can never run: something on the handle while the
633
+ * transaction that owns it is waiting for that very statement.
634
+ */
635
+ function statementInsideTransactionError() {
636
+ return new 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. ' +
637
+ 'SQLite has one connection here, so the call would either join that transaction (and be rolled back with ' +
638
+ 'it) or wait for it forever. Use the `tx` client the callback receives, or make the call after the ' +
639
+ 'transaction completes.');
640
+ }
641
+ /** The error a caller gets when an open transaction holds the handle past `busyTimeoutMs`. */
642
+ function handleBusyError(timeoutMs) {
643
+ return new SerializationFailureError({
644
+ message: `SQLite database is busy: a transaction on this client held it for longer than busyTimeoutMs (${timeoutMs} ms). ` +
645
+ 'Retry the operation (withRetry does), keep transactions short, or raise busyTimeoutMs.',
646
+ });
647
+ }
438
648
  /**
439
649
  * A `PgCompatPool` backed by a single `node:sqlite` `DatabaseSync` connection.
440
650
  * SQLite is single-connection by nature (a `:memory:` database is per-handle),
441
- * so `connect()` hands back a client over the **same** handle, transactions
442
- * (`BEGIN`/`COMMIT`/`ROLLBACK`, `SAVEPOINT` nesting) just run on it. Queries are
443
- * serialized; this is the documented single-writer model.
651
+ * so `connect()` hands back a client over the **same** handle.
652
+ *
653
+ * One handle carries one transaction, so the pool decides who may use it. A
654
+ * caller whose statement opens a transaction OWNS the handle until that
655
+ * transaction ends; every other caller's statements, plain queries included,
656
+ * wait their turn in FIFO order (bounded by `busyTimeoutMs`) instead of running
657
+ * inside it. Without that, a write issued outside an open transaction joined
658
+ * it, reported success, and was rolled back with it, and reads saw its
659
+ * uncommitted rows. A checkout that does not open a transaction (a stream, a
660
+ * connectivity check) holds nothing, so other callers keep running beside it.
444
661
  */
445
662
  export class SqlitePool {
446
663
  /** The underlying `node:sqlite` handle, exposed as an escape hatch (seed/DDL). */
447
664
  db;
448
665
  closed = false;
449
666
  /**
450
- * `sql -> declared-boolean result columns` (or `null` for none). Bounded like
667
+ * `sql -> result columns to decode` (or `null` for none). Bounded like
451
668
  * the query builder's SQL-template cache, since a caller can generate
452
669
  * unbounded distinct statements (raw SQL, an IN-list that inlines).
453
670
  */
454
- booleanCache = new LRUCache(1000);
455
- constructor(db) {
671
+ decoderCache = new LRUCache(1000);
672
+ /** Schema columns whose stored form is not their declared JS type (see {@link declaredDecoders}). */
673
+ declared;
674
+ busyTimeoutMs;
675
+ /** Who may run statements on the handle right now; `null` when nobody holds it. */
676
+ owner = null;
677
+ waiters = [];
678
+ /** The owner for statements sent through `pool.query()` rather than a checkout. */
679
+ poolOwner = {};
680
+ /** Transaction state when the driver cannot report it (`isTransaction` arrived in Node 22.16). */
681
+ trackedTransaction = false;
682
+ constructor(db, options = {}) {
456
683
  this.db = db;
684
+ const t = Number(options.busyTimeoutMs ?? DEFAULT_BUSY_TIMEOUT_MS);
685
+ this.busyTimeoutMs = Number.isFinite(t) && t >= 0 ? t : DEFAULT_BUSY_TIMEOUT_MS;
686
+ this.declared = declaredDecoders(options.schema);
457
687
  }
458
688
  // biome-ignore lint/suspicious/noExplicitAny: pg-compat query is generic over the row shape; runStatement returns plain objects.
459
689
  async query(text, values) {
460
690
  const { text: sql, params } = normalizeQueryArgs(text, values);
461
- return runStatement(this.db, sql, params, this.booleanCache);
691
+ return this.runAs(this.poolOwner, sql, params);
462
692
  }
463
693
  async connect() {
464
- const db = this.db;
465
- const booleanCache = this.booleanCache;
694
+ if (this.closed)
695
+ throw new ConnectionError('This SQLite client has been disconnected.');
696
+ const owner = {};
697
+ let released = false;
466
698
  return {
467
699
  // biome-ignore lint/suspicious/noExplicitAny: see query() above.
468
700
  query: async (text, values) => {
701
+ if (released) {
702
+ throw new ConnectionError('This SQLite connection was released (its transaction ended or timed out); ' +
703
+ 'run the statement through the client or a new transaction.');
704
+ }
469
705
  const { text: sql, params } = normalizeQueryArgs(text, values);
470
- return runStatement(db, sql, params, booleanCache);
706
+ return this.runAs(owner, sql, params);
471
707
  },
708
+ wrapTransactionCallback: (fn) => sqliteTxStorage.run({ pool: this, owner, parent: sqliteTxStorage.getStore() }, fn),
472
709
  release: () => {
473
- // Single shared connection, nothing to return to a pool.
710
+ if (released)
711
+ return;
712
+ released = true;
713
+ this.releaseOwner(owner);
474
714
  },
475
715
  };
476
716
  }
@@ -480,6 +720,103 @@ export class SqlitePool {
480
720
  this.closed = true;
481
721
  this.db.close();
482
722
  }
723
+ /** Is the handle inside a transaction right now? */
724
+ inTransaction() {
725
+ const reported = this.db.isTransaction;
726
+ return typeof reported === 'boolean' ? reported : this.trackedTransaction;
727
+ }
728
+ /** Run one statement for `owner`, waiting first if another caller's transaction holds the handle. */
729
+ async runAs(owner, sql, params) {
730
+ if (this.closed)
731
+ throw new ConnectionError('This SQLite client has been disconnected.');
732
+ if (this.owner !== owner) {
733
+ if (this.owner !== null || this.waiters.length > 0) {
734
+ if (this.calledFromOwnersCallback())
735
+ throw statementInsideTransactionError();
736
+ await this.waitForHandle(owner);
737
+ }
738
+ else {
739
+ this.owner = owner;
740
+ }
741
+ }
742
+ try {
743
+ const result = runStatement(this.db, sql, params, this.decoderCache, this.declared);
744
+ if (TX_OPEN_HEAD.test(sql))
745
+ this.trackedTransaction = true;
746
+ else if (TX_CLOSE_HEAD.test(sql))
747
+ this.trackedTransaction = false;
748
+ return result;
749
+ }
750
+ finally {
751
+ // A statement that leaves no transaction open hands the handle on.
752
+ if (this.owner === owner && !this.inTransaction())
753
+ this.handOff();
754
+ }
755
+ }
756
+ /** True when the current async context is the callback of the transaction that owns the handle. */
757
+ calledFromOwnersCallback() {
758
+ for (let m = sqliteTxStorage.getStore(); m; m = m.parent) {
759
+ if (m.pool === this && m.owner === this.owner)
760
+ return true;
761
+ }
762
+ return false;
763
+ }
764
+ waitForHandle(owner) {
765
+ return new Promise((resolve, reject) => {
766
+ let timer;
767
+ const waiter = {
768
+ owner,
769
+ settled: false,
770
+ grant: () => {
771
+ if (timer)
772
+ clearTimeout(timer);
773
+ resolve();
774
+ },
775
+ };
776
+ this.waiters.push(waiter);
777
+ timer = setTimeout(() => {
778
+ if (waiter.settled)
779
+ return;
780
+ waiter.settled = true;
781
+ const at = this.waiters.indexOf(waiter);
782
+ if (at >= 0)
783
+ this.waiters.splice(at, 1);
784
+ reject(handleBusyError(this.busyTimeoutMs));
785
+ }, this.busyTimeoutMs);
786
+ });
787
+ }
788
+ /** Give the handle to the next waiter, which runs its statement as soon as it resumes. */
789
+ handOff() {
790
+ this.owner = null;
791
+ while (this.waiters.length > 0) {
792
+ const next = this.waiters.shift();
793
+ if (next.settled)
794
+ continue;
795
+ next.settled = true;
796
+ this.owner = next.owner;
797
+ next.grant();
798
+ return;
799
+ }
800
+ }
801
+ /**
802
+ * A checkout ended. If it still holds an open transaction (a timed-out or
803
+ * abandoned callback), roll it back first, so the next caller does not
804
+ * start inside it.
805
+ */
806
+ releaseOwner(owner) {
807
+ if (this.owner !== owner)
808
+ return;
809
+ if (this.inTransaction() && !this.closed) {
810
+ try {
811
+ this.db.exec('ROLLBACK');
812
+ }
813
+ catch {
814
+ // Nothing left to roll back.
815
+ }
816
+ this.trackedTransaction = false;
817
+ }
818
+ this.handOff();
819
+ }
483
820
  }
484
821
  // ---------------------------------------------------------------------------
485
822
  // Type mapping (SQLite declared type → TypeScript)
@@ -547,6 +884,44 @@ function sqliteColumnAffinity(type) {
547
884
  export const sqliteDialect = {
548
885
  ...postgresDialect,
549
886
  name: 'sqlite',
887
+ capabilities: Object.freeze({
888
+ distinctOn: false,
889
+ positionalJson: false,
890
+ // NULLS FIRST / LAST since 3.30; NULL is the smallest value.
891
+ nullsOrderingClause: true,
892
+ nullsSortHigh: false,
893
+ insensitiveInList: false,
894
+ jsonDocumentEquality: 'text',
895
+ jsonExtractTyped: true,
896
+ jsonExactNumericCast: false,
897
+ numericTextComparison: false,
898
+ // Enforced only on a connection that turned `foreign_keys` on.
899
+ enforcesForeignKeys: false,
900
+ partitionLimitPushdown: false,
901
+ utcTimestampRewrite: false,
902
+ paramTypeCasts: false,
903
+ namedEnumTypes: false,
904
+ structuredValueColumns: 'scalars',
905
+ // The default SQLITE_MAX_VARIABLE_NUMBER.
906
+ maxBindParameters: 32766,
907
+ maxInsertRows: Number.POSITIVE_INFINITY,
908
+ upsertConflictTarget: 'explicit',
909
+ setClauseOrder: 'snapshot',
910
+ reselectLocksRows: false,
911
+ integerDivision: '/',
912
+ updateExpressions: true,
913
+ bulkUpsertAppliesRepeatedKeys: true,
914
+ readOnlyTransactions: false,
915
+ connectionParameters: false,
916
+ backendCancel: false,
917
+ idleTransactionTimeout: false,
918
+ abortsTransactionOnError: false,
919
+ connectionDriver: 'engine-driver',
920
+ preparesEveryStatement: false,
921
+ statementSyntax: 'sqlite',
922
+ inlineForeignKeys: true,
923
+ unboundedTextKeys: true,
924
+ }),
550
925
  resultStrategy: 'returning', // SQLite ≥ 3.35 has RETURNING
551
926
  supportsReturning: true,
552
927
  supportsILike: false,
@@ -563,6 +938,13 @@ export const sqliteDialect = {
563
938
  // No FROM-clause LATERAL: the opt-in lateral pick plan is Postgres-only.
564
939
  supportsLateralJoin: false,
565
940
  supportsPlanCacheMode: false,
941
+ // No row locks: a write transaction locks the whole database.
942
+ rowLockModes: [],
943
+ // percentile_cont / percentile_disc exist only in builds compiled with
944
+ // SQLITE_ENABLE_PERCENTILE, which node:sqlite cannot be assumed to have.
945
+ supportsPercentileAggregates: false,
946
+ // INSERT ... ON CONFLICT (cols) DO UPDATE ... RETURNING (3.24 + 3.35).
947
+ supportsBulkUpsert: true,
566
948
  // SQLite explains a compiled query with `EXPLAIN QUERY PLAN` (four columns:
567
949
  // id, parent, notused, detail), overriding the inherited Postgres `EXPLAIN`.
568
950
  explainQuery: { prefix: 'EXPLAIN QUERY PLAN' },
@@ -626,7 +1008,7 @@ export const sqliteDialect = {
626
1008
  }
627
1009
  // A declared-boolean column is 1/0 in storage, and the driver shim turns
628
1010
  // that back into true/false on every direct read (see
629
- // booleanResultColumns). `json_object` sees the raw integer and has no
1011
+ // resultColumnDecoders). `json_object` sees the raw integer and has no
630
1012
  // decltype to consult, so the join strategy alone would have kept handing
631
1013
  // back 1/0 while the top-level read, the batched loader and the flatten
632
1014
  // plan returned booleans, which is the same strategy-dependent type flip
@@ -638,7 +1020,8 @@ export const sqliteDialect = {
638
1020
  // driverReportsDeclaredTypes). Without it the shim cannot name a boolean
639
1021
  // result column, so 1/0 is the answer everywhere; converting here alone
640
1022
  // would put the flip back, pointing the other way.
641
- if (t.includes('BOOL') && driverReportsDeclaredTypes()) {
1023
+ const decode = sqliteValueDecode(columnType);
1024
+ if (decode === 'boolean' && driverReportsDeclaredTypes()) {
642
1025
  return {
643
1026
  // Carried as TEXT because that is how the decode half is reached at
644
1027
  // all: the parser only runs `decode` on a string cell, so a rule that
@@ -651,6 +1034,29 @@ export const sqliteDialect = {
651
1034
  decode: (value) => (value === '1' ? true : value === '0' ? false : value),
652
1035
  };
653
1036
  }
1037
+ // A json/jsonb column is JSON text in storage, which the shim parses on
1038
+ // every direct read (see resultColumnDecoders). `json_object` embeds the
1039
+ // same text as a JSON string, so the decode is the same parse. No cast: the
1040
+ // column reference already yields the text. Gated like the boolean rule,
1041
+ // since the direct-read half needs the same driver capability.
1042
+ if (decode === 'json' && driverReportsDeclaredTypes()) {
1043
+ return { sql: (ref) => ref, decode: parseJsonCell };
1044
+ }
1045
+ // `json_object` renders a REAL with 15 significant digits on the SQLite
1046
+ // that Node 22 and 24 bundle (3.47 to 3.51; 3.53 renders 17), so 0.1 + 0.2
1047
+ // came back 0.3 through a join and a NUMERIC past 15 digits was rounded
1048
+ // again on top of its REAL storage, while the top-level read and the
1049
+ // batched loader return the stored double. `printf('%!.17g')` prints every
1050
+ // double with enough digits to rebuild it, on every version; an INTEGER in
1051
+ // the same column is carried as its text and decoded by the safe-integer
1052
+ // policy above; text and NULL pass through as stored.
1053
+ if (/REAL|FLOA|DOUB|NUMERIC|DECIMAL|MONEY/.test(t)) {
1054
+ return {
1055
+ sql: (ref) => `CASE typeof(${ref}) WHEN 'real' THEN printf('%!.17g', ${ref}) ` +
1056
+ `WHEN 'integer' THEN CAST(${ref} AS TEXT) ELSE ${ref} END`,
1057
+ decode: decodeRealText,
1058
+ };
1059
+ }
654
1060
  // A BLOB cannot go into JSON at all: SQLite raises "JSON cannot hold BLOB
655
1061
  // values" and the whole query fails with a raw SQL logic error, where the
656
1062
  // batched loader returns the row fine. Carry hex and rebuild the bytes.
@@ -684,11 +1090,35 @@ export const sqliteDialect = {
684
1090
  // No `= ANY(array)` in SQLite. `json_each` expands a single JSON-array param
685
1091
  // into a row set, keeping ONE placeholder (so the SQL cache stays valid
686
1092
  // regardless of list length) and handling the empty-list case correctly.
1093
+ // `x IN (SELECT ...)` compares like `x = ?`: the column's affinity and
1094
+ // collation apply to each element, as they do to a bound operand. JSON has no
1095
+ // bytes, so a binary element travels as `{"b": "<hex>"}` and is decoded back
1096
+ // to a BLOB here (see sqliteInListJson).
687
1097
  buildInClause(expr, paramRef, negated) {
688
- return `${expr} ${negated ? 'NOT IN' : 'IN'} (SELECT value FROM json_each(${paramRef}))`;
1098
+ return (`${expr} ${negated ? 'NOT IN' : 'IN'} ` +
1099
+ `(SELECT CASE WHEN type = 'object' THEN unhex(json_extract(value, '$.b')) ELSE value END ` +
1100
+ `FROM json_each(${paramRef}))`);
689
1101
  },
690
1102
  inClauseParam(values) {
691
- return JSON.stringify(values ?? []);
1103
+ return sqliteInListJson(Array.isArray(values) ? values : []);
1104
+ },
1105
+ /**
1106
+ * A string written to a numeric or boolean column. SQLite's type affinity
1107
+ * keeps text it cannot convert as TEXT in that column, where PostgreSQL
1108
+ * refuses it at the server: `'abc'` in an INTEGER column, or `'true'` in a
1109
+ * boolean one (stored 1/0), which `{ flag: true }` then never matches. A
1110
+ * string that is a number (`'12.5'`, `'9007199254740993'`, `' 7 '`) is
1111
+ * accepted and converted by SQLite, as before.
1112
+ */
1113
+ checkTextWrite(table, column, columnType, value) {
1114
+ if (!SQLITE_NUMERIC_COLUMN_TYPE.test(columnType.trim()))
1115
+ return undefined;
1116
+ if (SQLITE_NUMERIC_LITERAL.test(value))
1117
+ return undefined;
1118
+ const boolean = /BOOL/i.test(columnType);
1119
+ return (`The string written to "${column}" on "${table}", a column of type ${columnType}, is not a number, so SQLite ` +
1120
+ `would store it as text in that column; PostgreSQL refuses it. ` +
1121
+ (boolean ? 'Write true or false.' : 'Write a number, or a string holding one, such as "12.5".'));
692
1122
  },
693
1123
  buildReturningClause(selection = '*') {
694
1124
  return ` RETURNING ${selection === '*' ? '*' : selection.join(', ')}`;
@@ -711,6 +1141,21 @@ export const sqliteDialect = {
711
1141
  params: input.rowValues.flat(),
712
1142
  };
713
1143
  },
1144
+ buildBulkUpsertStatement(input) {
1145
+ // VALUES, not `INSERT ... SELECT`: after a SELECT, SQLite cannot tell a
1146
+ // trailing `ON CONFLICT` from the SELECT's own clauses. The rest is the
1147
+ // PostgreSQL grammar (3.24 upsert, 3.35 RETURNING), `excluded` included.
1148
+ let n = 0;
1149
+ const tuples = input.rowValues.map((row) => `(${row.map(() => this.paramPlaceholder(++n)).join(', ')})`).join(', ');
1150
+ const params = input.rowValues.flat();
1151
+ return {
1152
+ sql: `INSERT INTO ${input.table} (${input.columns.join(', ')}) VALUES ${tuples} ` +
1153
+ `ON CONFLICT (${input.conflictColumns.join(', ')}) ` +
1154
+ onConflictAction(input, params, { target: input.table, incoming: 'excluded' }) +
1155
+ this.buildReturningClause(input.returning),
1156
+ params,
1157
+ };
1158
+ },
714
1159
  buildDefaultValuesInsertStatement(input) {
715
1160
  // SQLite's `DEFAULT VALUES` inserts exactly one row and the engine accepts
716
1161
  // neither `VALUES (DEFAULT, …)` nor MySQL's empty `VALUES ()` tuple, so
@@ -731,6 +1176,12 @@ export const sqliteDialect = {
731
1176
  (input.updateWhere ? ` WHERE ${input.updateWhere}` : '') +
732
1177
  this.buildReturningClause(input.returning));
733
1178
  },
1179
+ // A SQLite transaction is always SERIALIZABLE, which satisfies every level
1180
+ // `$transaction({ isolationLevel })` can ask for, and the inherited
1181
+ // `BEGIN ISOLATION LEVEL ...` is a syntax error here.
1182
+ beginStatement() {
1183
+ return 'BEGIN';
1184
+ },
734
1185
  buildInsensitiveLike(column, paramRef) {
735
1186
  // COLLATE NOCASE is ASCII-only (no Unicode case folding), documented limit.
736
1187
  return `${column} LIKE ${paramRef} COLLATE NOCASE`;
@@ -759,6 +1210,11 @@ export const sqliteDialect = {
759
1210
  buildColumnType(input) {
760
1211
  return sqliteColumnAffinity(input.type);
761
1212
  },
1213
+ /** SQLite has no enum type: a TEXT column, and a CHECK that admits only the labels (compared exactly, BINARY collation). */
1214
+ buildEnumColumn(column, labels) {
1215
+ const literals = labels.map((label) => `'${label.replace(/'/g, "''")}'`);
1216
+ return { type: 'TEXT', check: `${column} IN (${literals.join(', ')})` };
1217
+ },
762
1218
  buildColumnDefinition(input) {
763
1219
  const isSerial = /serial/i.test(input.type);
764
1220
  const parts = [input.name];
@@ -934,16 +1390,19 @@ export function introspectSqliteDatabase(db, options = {}) {
934
1390
  // same logical schema, legacy-first naming, per-column disambiguation,
935
1391
  // collision resolution against scalar column fields, and the conservative
936
1392
  // pure-junction manyToMany auto-detection included.
937
- const relationsByTable = deriveEngineRelations(tableNames, foreignKeys, pkByTable, columnsByTable);
1393
+ // One field per column, as the PostgreSQL introspector requires.
1394
+ for (const [tableName, cols] of columnsByTable)
1395
+ assertDistinctColumnFields(tableName, cols);
1396
+ const relationsByTable = deriveEngineRelations(tableNames, foreignKeys, pkByTable, columnsByTable, options.logger);
938
1397
  // ----- Assemble TableMetadata -----
939
- const tables = {};
1398
+ const tables = nullRecord();
940
1399
  for (const tableName of tableNames) {
941
1400
  const columns = columnsByTable.get(tableName) ?? [];
942
- const columnMap = {};
943
- const reverseColumnMap = {};
1401
+ const columnMap = nullRecord();
1402
+ const reverseColumnMap = nullRecord();
944
1403
  const dateColumns = new Set();
945
- const dialectTypes = {};
946
- const pgTypes = {};
1404
+ const dialectTypes = nullRecord();
1405
+ const pgTypes = nullRecord();
947
1406
  const allColumns = [];
948
1407
  for (const col of columns) {
949
1408
  columnMap[col.field] = col.name;
@@ -965,7 +1424,7 @@ export function introspectSqliteDatabase(db, options = {}) {
965
1424
  allColumns,
966
1425
  primaryKey: pkByTable.get(tableName) ?? [],
967
1426
  uniqueColumns: uniqueByTable.get(tableName) ?? [],
968
- relations: relationsByTable.get(tableName) ?? {},
1427
+ relations: relationsByTable.get(tableName) ?? nullRecord(),
969
1428
  indexes: indexesByTable.get(tableName) ?? [],
970
1429
  };
971
1430
  }
@@ -990,7 +1449,7 @@ export async function introspectSqlite(options) {
990
1449
  throw new ConnectionError(`Failed to open SQLite database "${options.connectionString}": ${err.message}`);
991
1450
  }
992
1451
  try {
993
- return introspectSqliteDatabase(db, { include: options.include, exclude: options.exclude });
1452
+ return introspectSqliteDatabase(db, { include: options.include, exclude: options.exclude, logger: options.logger });
994
1453
  }
995
1454
  finally {
996
1455
  db.close();
@@ -1005,9 +1464,17 @@ function openSqliteDatabase(target, options) {
1005
1464
  catch (err) {
1006
1465
  throw new ConnectionError(`Failed to open SQLite database "${target}": ${err.message}`);
1007
1466
  }
1008
- db.exec(`PRAGMA busy_timeout = ${Number(options.busyTimeoutMs ?? 5000)}`);
1009
- if (options.foreignKeys !== false)
1010
- db.exec('PRAGMA foreign_keys = ON');
1467
+ try {
1468
+ db.exec(`PRAGMA busy_timeout = ${Number(options.busyTimeoutMs ?? 5000)}`);
1469
+ if (options.foreignKeys !== false)
1470
+ db.exec('PRAGMA foreign_keys = ON');
1471
+ }
1472
+ catch (err) {
1473
+ // Opened here and about to be unreachable: close it rather than leave the
1474
+ // file handle to the garbage collector.
1475
+ db.close();
1476
+ throw err;
1477
+ }
1011
1478
  if (target !== ':memory:' && options.wal !== false) {
1012
1479
  try {
1013
1480
  db.exec('PRAGMA journal_mode = WAL');
@@ -1038,16 +1505,47 @@ function openSqliteDatabase(target, options) {
1038
1505
  * ```
1039
1506
  */
1040
1507
  export function turbineSqlite(target, schema, options = {}) {
1041
- const db = typeof target === 'string' ? openSqliteDatabase(target, options) : target;
1042
- const pool = new SqlitePool(db);
1043
- // Everything that is not a SQLite pragma is client config and rides through
1044
- // untouched, so a new TurbineConfig option works here the day it lands. The
1045
- // engine-owned keys come last, so no caller can unbind the dialect.
1046
- const { wal: _wal, busyTimeoutMs: _busyTimeoutMs, foreignKeys: _foreignKeys, ...clientConfig } = options;
1047
- return new TurbineClient({
1048
- ...clientConfig,
1049
- pool,
1050
- dialect: sqliteDialect,
1051
- preparedStatements: false,
1052
- }, schema);
1508
+ const owns = typeof target === 'string';
1509
+ const db = owns ? openSqliteDatabase(target, options) : target;
1510
+ let pool;
1511
+ let client;
1512
+ try {
1513
+ pool = new SqlitePool(db, { busyTimeoutMs: options.busyTimeoutMs, schema });
1514
+ // Everything that is not a SQLite pragma is client config and rides through
1515
+ // untouched, so a new TurbineConfig option works here the day it lands. The
1516
+ // engine-owned keys come last, so no caller can unbind the dialect.
1517
+ const { wal: _wal, busyTimeoutMs: _busyTimeoutMs, foreignKeys: _foreignKeys, ...clientConfig } = options;
1518
+ client = new TurbineClient({
1519
+ ...clientConfig,
1520
+ pool,
1521
+ dialect: sqliteDialect,
1522
+ preparedStatements: false,
1523
+ }, schema);
1524
+ }
1525
+ catch (err) {
1526
+ // A failed factory hands the caller nothing to close, so a handle it
1527
+ // OPENED is closed here (the client constructor refuses a bad config, a
1528
+ // static global filter naming no table or column for one). A handle the
1529
+ // caller passed in is the caller's and stays open.
1530
+ if (owns)
1531
+ db.close();
1532
+ throw err;
1533
+ }
1534
+ if (owns) {
1535
+ // Turbine opened this handle, so disconnect()/end() must close it, as the
1536
+ // other engine factories close a pool they built: the client sees
1537
+ // `TurbineConfig.pool` as external and would otherwise leave it open, still
1538
+ // answering queries after disconnect(). A handle the caller passed in stays
1539
+ // the caller's (disconnect() is a no-op for it).
1540
+ const baseDisconnect = client.disconnect.bind(client);
1541
+ const close = async (options) => {
1542
+ const result = options === undefined ? await baseDisconnect() : await baseDisconnect(options);
1543
+ await pool.end();
1544
+ return result ?? undefined;
1545
+ };
1546
+ const patch = client;
1547
+ patch.disconnect = close;
1548
+ patch.end = close;
1549
+ }
1550
+ return client;
1053
1551
  }