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
@@ -18,6 +18,7 @@
18
18
  */
19
19
  import pg from '#pg';
20
20
  import { PipelineError, TimeoutError, wrapPgError } from './errors.js';
21
+ import { rolledBackError } from './transaction-state.js';
21
22
  // Read off pg's own module object (`pg.Result` IS `pg/lib/result`, `pg.utils`
22
23
  // IS `pg/lib/utils`) rather than imported by subpath, so the whole driver comes
23
24
  // through the one `#pg` alias that edge bundles swap out (see src/pg-edge.ts).
@@ -86,6 +87,16 @@ function restoreListeners(emitter, snapshot) {
86
87
  // ---------------------------------------------------------------------------
87
88
  // Core pipeline execution
88
89
  // ---------------------------------------------------------------------------
90
+ /** A statement that only reads: its transform may run after COMMIT, since there is nothing to undo. */
91
+ const READ_ONLY_STATEMENT = /^\s*select\b/i;
92
+ /**
93
+ * Whether a transactional batch must hold its COMMIT until the transforms have
94
+ * run. Anything that is not a plain SELECT counts as a write, so a statement
95
+ * this cannot classify costs a round trip rather than correctness.
96
+ */
97
+ export function batchMayWrite(queries) {
98
+ return queries.some((q) => !READ_ONLY_STATEMENT.test(q.sql));
99
+ }
89
100
  /**
90
101
  * Execute multiple queries using the Postgres extended-query pipeline protocol.
91
102
  *
@@ -112,11 +123,20 @@ export async function runPipelined(client, queries, options = {}) {
112
123
  // State machine
113
124
  // -----------------------------------------------------------------------
114
125
  /**
115
- * Total expected commandComplete events:
116
- * - Transactional: BEGIN + N queries + COMMIT = N + 2
117
- * - Non-transactional: N queries
126
+ * True when the batch is sent as BEGIN + queries + Sync and the COMMIT
127
+ * follows only once every transform has succeeded (see
128
+ * {@link PipelineRunOptions.transactional}).
118
129
  */
119
- const totalCommands = transactional ? queries.length + 2 : queries.length;
130
+ const holdCommit = transactional && batchMayWrite(queries);
131
+ /**
132
+ * Where the exchange is: the batch itself, or the single follow-up
133
+ * statement (the held COMMIT, or the recovery ROLLBACK) sent after it.
134
+ */
135
+ let phase = 'batch';
136
+ /** The command tag the held COMMIT came back with (`ROLLBACK` when the server rolled back). */
137
+ let commitTag;
138
+ /** Transformed results, computed before a held COMMIT is sent. */
139
+ let transformed;
120
140
  /**
121
141
  * Expected readyForQuery events:
122
142
  * - Transactional: 1 (single Sync at end)
@@ -142,20 +162,17 @@ export async function runPipelined(client, queries, options = {}) {
142
162
  let cleaned = false;
143
163
  /** Transaction status reported by the most recent ReadyForQuery. */
144
164
  let txStatus;
145
- /** Whether the recovery ROLLBACK has already been sent (it is sent at most once). */
146
- let rollbackSent = false;
147
165
  /**
148
166
  * Map commandComplete index to the corresponding results[] index.
149
167
  * In transactional mode: index 0 = BEGIN, 1..N = queries, N+1 = COMMIT
150
- * In non-transactional mode: index maps 1:1
168
+ * (absent when the COMMIT is held). In non-transactional mode: 1:1.
169
+ * Follow-up statements belong to no query.
151
170
  */
152
171
  function commandToQueryIndex(cmdIdx) {
153
- if (transactional) {
154
- if (cmdIdx === 0 || cmdIdx === totalCommands - 1)
155
- return null;
156
- return cmdIdx - 1;
157
- }
158
- return cmdIdx;
172
+ if (phase !== 'batch')
173
+ return null;
174
+ const qIdx = transactional ? cmdIdx - 1 : cmdIdx;
175
+ return qIdx >= 0 && qIdx < queries.length ? qIdx : null;
159
176
  }
160
177
  /** Get the current query-results index for row data */
161
178
  function currentQueryIndex() {
@@ -214,30 +231,56 @@ export async function runPipelined(client, queries, options = {}) {
214
231
  return true;
215
232
  if (txStatus === undefined && !(transactional && pipelineError))
216
233
  return true;
217
- if (!transactional || rollbackSent) {
218
- // Either nothing to roll back (non-transactional mode opens no
219
- // transaction of its own), or the rollback already ran and did not
220
- // clear the status. Hand the client back marked for disposal.
221
- if (txStatus === 'E' || txStatus === 'T') {
222
- Object.defineProperty(client, PIPELINE_DISCARD, { value: true, enumerable: false, configurable: true });
223
- }
234
+ if (!transactional) {
235
+ // Non-transactional mode opens no transaction of its own, so there is
236
+ // nothing to roll back. Hand the client back marked for disposal.
237
+ markDiscardIfInTransaction();
224
238
  return true;
225
239
  }
226
- rollbackSent = true;
240
+ return !sendFollowUp('ROLLBACK', 'rollback');
241
+ }
242
+ /** Mark the client for disposal when the backend still reports an open or failed transaction. */
243
+ function markDiscardIfInTransaction() {
244
+ if (txStatus === 'E' || txStatus === 'T') {
245
+ Object.defineProperty(client, PIPELINE_DISCARD, { value: true, enumerable: false, configurable: true });
246
+ }
247
+ }
248
+ /**
249
+ * Send one statement after the batch (the held COMMIT or the recovery
250
+ * ROLLBACK) with its own Sync; its ReadyForQuery finishes the exchange.
251
+ * Returns false when the socket would not take it, in which case the
252
+ * connection is marked for disposal, since it cannot be repaired from here.
253
+ */
254
+ function sendFollowUp(text, next) {
255
+ phase = next;
227
256
  try {
228
- connection.parse({ text: 'ROLLBACK', name: '' });
229
- connection.bind({ portal: '', statement: '', values: [], valueMapper: prepareValue });
230
- connection.execute({ portal: '', rows: 0 });
231
- connection.sync();
232
- return false;
257
+ connection.stream.cork?.();
258
+ try {
259
+ connection.parse({ text, name: '' });
260
+ connection.bind({ portal: '', statement: '', values: [], valueMapper: prepareValue });
261
+ connection.execute({ portal: '', rows: 0 });
262
+ connection.sync();
263
+ }
264
+ finally {
265
+ connection.stream.uncork?.();
266
+ }
267
+ return true;
233
268
  }
234
- catch {
235
- // The socket would not take the rollback, so this connection cannot be
236
- // repaired from here. Mark it and let the caller drop it.
269
+ catch (err) {
237
270
  Object.defineProperty(client, PIPELINE_DISCARD, { value: true, enumerable: false, configurable: true });
238
- return true;
271
+ if (!pipelineError)
272
+ pipelineError = err instanceof Error ? err : new Error(String(err));
273
+ return false;
239
274
  }
240
275
  }
276
+ /** Run every query's transform, in order; the first throw propagates. */
277
+ function transformAll() {
278
+ const out = [];
279
+ for (let i = 0; i < queries.length; i++) {
280
+ out.push(queries[i].transform(results[i]));
281
+ }
282
+ return out;
283
+ }
241
284
  function finalize() {
242
285
  cleanup();
243
286
  if (transactional && pipelineError) {
@@ -280,14 +323,14 @@ export async function runPipelined(client, queries, options = {}) {
280
323
  }));
281
324
  return;
282
325
  }
283
- // All succeeded, transform results
284
- try {
285
- const transformed = [];
286
- for (let i = 0; i < queries.length; i++) {
287
- const q = queries[i];
288
- transformed.push(q.transform(results[i]));
289
- }
326
+ // All succeeded. A held COMMIT ran the transforms before committing;
327
+ // a read-only batch runs them now (there is nothing to undo).
328
+ if (transformed) {
290
329
  resolve(transformed);
330
+ return;
331
+ }
332
+ try {
333
+ resolve(transformAll());
291
334
  }
292
335
  catch (err) {
293
336
  reject(err);
@@ -320,6 +363,8 @@ export async function runPipelined(client, queries, options = {}) {
320
363
  }
321
364
  }
322
365
  function onCommandComplete(msg) {
366
+ if (phase === 'commit')
367
+ commitTag = msg.command ?? msg.text;
323
368
  const qIdx = currentQueryIndex();
324
369
  if (qIdx !== null && qIdx >= 0 && qIdx < results.length) {
325
370
  results[qIdx].addCommandComplete(msg);
@@ -366,15 +411,41 @@ export async function runPipelined(client, queries, options = {}) {
366
411
  }
367
412
  function onReadyForQuery(msg) {
368
413
  txStatus = msg?.status;
369
- // The recovery ROLLBACK's own ReadyForQuery is not one of the batch's, so
414
+ // A follow-up statement's own ReadyForQuery is not one of the batch's, so
370
415
  // it must not advance the counter that decides when the batch is done.
371
- if (rollbackSent) {
416
+ if (phase === 'rollback') {
417
+ markDiscardIfInTransaction();
418
+ finalize();
419
+ return;
420
+ }
421
+ if (phase === 'commit') {
422
+ // PostgreSQL answers COMMIT in a failed transaction with the tag
423
+ // ROLLBACK and no error. Nothing here sends COMMIT after an error, but
424
+ // the answer is checked rather than assumed.
425
+ if (!pipelineError && commitTag === 'ROLLBACK')
426
+ pipelineError = rolledBackError(undefined, 'transaction');
427
+ markDiscardIfInTransaction();
372
428
  finalize();
373
429
  return;
374
430
  }
375
431
  rfqCount++;
376
432
  if (rfqCount < expectedRfq)
377
433
  return;
434
+ if (holdCommit && !pipelineError && txStatus !== 'E') {
435
+ try {
436
+ transformed = transformAll();
437
+ }
438
+ catch (err) {
439
+ // A transform refused its result: roll the batch's writes back.
440
+ pipelineError = err instanceof Error ? err : new Error(String(err));
441
+ if (!sendFollowUp('ROLLBACK', 'rollback'))
442
+ finalize();
443
+ return;
444
+ }
445
+ if (!sendFollowUp('COMMIT', 'commit'))
446
+ finalize();
447
+ return;
448
+ }
378
449
  if (settleTransactionState())
379
450
  finalize();
380
451
  }
@@ -421,7 +492,8 @@ export async function runPipelined(client, queries, options = {}) {
421
492
  if (transactional) {
422
493
  // ---- Transactional mode ----
423
494
  // BEGIN + N×(parse+bind+describe+execute) + COMMIT + sync
424
- // One sync = one ReadyForQuery
495
+ // One sync = one ReadyForQuery. A batch that may write leaves the
496
+ // COMMIT out here and sends it after the transforms (holdCommit).
425
497
  // BEGIN
426
498
  connection.parse({ text: 'BEGIN', name: '' });
427
499
  connection.bind({ portal: '', statement: '', values: [], valueMapper: prepareValue });
@@ -438,10 +510,11 @@ export async function runPipelined(client, queries, options = {}) {
438
510
  connection.describe({ type: 'P', name: '' });
439
511
  connection.execute({ portal: '', rows: 0 });
440
512
  }
441
- // COMMIT
442
- connection.parse({ text: 'COMMIT', name: '' });
443
- connection.bind({ portal: '', statement: '', values: [], valueMapper: prepareValue });
444
- connection.execute({ portal: '', rows: 0 });
513
+ if (!holdCommit) {
514
+ connection.parse({ text: 'COMMIT', name: '' });
515
+ connection.bind({ portal: '', statement: '', values: [], valueMapper: prepareValue });
516
+ connection.execute({ portal: '', rows: 0 });
517
+ }
445
518
  // Single Sync
446
519
  connection.sync();
447
520
  }
package/dist/pipeline.js CHANGED
@@ -18,10 +18,21 @@
18
18
  * Sequential fallback covers HTTP-based drivers (Neon HTTP, Vercel Postgres, Cloudflare
19
19
  * Hyperdrive), mock pools in tests, and any pool that doesn't expose pg internals.
20
20
  */
21
+ import { acquireConnection } from './checkout.js';
21
22
  import { guardCheckout } from './connection-guard.js';
22
23
  import { postgresDialect } from './dialect.js';
23
24
  import { explainConnectionLoss, PipelineError, TurbineError, wrapPgError } from './errors.js';
24
25
  import { pipelineClientNeedsDiscard, runPipelined, supportsExtendedPipeline, } from './pipeline-submittable.js';
26
+ import { commitTransaction } from './transaction-state.js';
27
+ /**
28
+ * Run one deferred query on the batch's connection. An engine without
29
+ * RETURNING (MySQL) attaches a `reselect` plan that runs the write and then
30
+ * fetches its row; sending `q.sql` alone would hand the transform a row-less
31
+ * result, which a single-row write reads as "not found".
32
+ */
33
+ function runOne(client, q) {
34
+ return q.reselect ? q.reselect((sql, params) => client.query(sql, params)) : client.query(q.sql, q.params);
35
+ }
25
36
  /**
26
37
  * Execute queries sequentially on an already-acquired connection.
27
38
  * This is the fallback path for clients that don't support the extended-query
@@ -51,19 +62,16 @@ async function runSequential(client, queries, dialect, options = {}) {
51
62
  for (const q of queries) {
52
63
  let raw;
53
64
  try {
54
- raw = await client.query(q.sql, q.params);
65
+ raw = await runOne(client, q);
55
66
  }
56
67
  catch (err) {
57
68
  throw wrapPgError(err);
58
69
  }
70
+ // Transformed BEFORE the COMMIT, so a transform that refuses its result
71
+ // (a `...OrThrow` miss, an optimistic-lock conflict) rolls the batch back.
59
72
  results.push(q.transform(raw));
60
73
  }
61
- try {
62
- await client.query(dialect.commitStatement());
63
- }
64
- catch (err) {
65
- throw wrapPgError(err);
66
- }
74
+ await commitTransaction(client, dialect.commitStatement());
67
75
  return results;
68
76
  }
69
77
  catch (err) {
@@ -141,7 +149,7 @@ async function runIndependent(client, queries) {
141
149
  const q = queries[i];
142
150
  let raw;
143
151
  try {
144
- raw = await client.query(q.sql, q.params);
152
+ raw = await runOne(client, q);
145
153
  }
146
154
  catch (err) {
147
155
  const wrapped = wrapPgError(err);
@@ -188,24 +196,11 @@ export async function executePipeline(pool, queries, options) {
188
196
  if (queries.length === 0) {
189
197
  return [];
190
198
  }
191
- // Acquire a single client, reused for both capability check and execution.
192
- // The checkout is wrapped because a connection-level failure (28P01 wrong
193
- // password, an unverifiable TLS cert, ECONNREFUSED) happens HERE, before any
194
- // query runs. Unwrapped it escapes as a raw pg DatabaseError whose `.code`
195
- // holds a SQLSTATE, which is the same property Turbine puts TURBINE_E0NN in,
196
- // so a caller switching on `.code` silently receives a value from a foreign
197
- // namespace. The transaction and nested-write checkouts were wrapped for this
198
- // reason; this was the last sibling still bare.
199
- let client;
200
- try {
201
- client = await pool.connect();
202
- }
203
- catch (err) {
204
- throw wrapPgError(err);
205
- }
206
- // Guarded for the checkout window, see connection-guard.ts: a connection that
207
- // dies mid-batch emits 'error', which with no listener exits the process.
208
- const checkout = guardCheckout(client);
199
+ // One client, reused for both capability check and execution. The shared
200
+ // checkout types a connection-level failure (wrong password, TLS, refused,
201
+ // an exhausted pool) and guards the checkout window: a connection that dies
202
+ // mid-batch emits 'error', which with no listener exits the process.
203
+ const { client, checkout } = await acquireConnection(pool);
209
204
  try {
210
205
  if (supportsExtendedPipeline(client)) {
211
206
  // Real pipeline path, uses extended-query protocol wire methods
@@ -13,8 +13,8 @@
13
13
  * shape the SQL introspectors produce, so a code-first PowDB database can be
14
14
  * introspected for bootstrap/verification. It is transport-agnostic: the caller
15
15
  * supplies an `exec(powql)` that returns row objects **keyed by column name**.
16
- * - Embedded / owned pool: `exec = async (q) => ({ rows: await db.raw([q]) })`
17
- * using a live `turbinePowDB` client's `raw` tagged template.
16
+ * - Embedded / owned pool: `exec = async (q) => ({ rows: await db.rawUnsafe(q) })`
17
+ * using a live `turbinePowDB` client.
18
18
  * - Networked: the raw `@zvndev/powdb-client` returns POSITIONAL rows
19
19
  * (`{ columns: string[], rows: string[][] }`), so zip them into records.
20
20
  * A bare `(await client.query(q)).rows` would hand this function `string[][]`
@@ -66,6 +66,7 @@
66
66
  * wired to a networked `exec`, and `cli/config.ts` teaching the generate
67
67
  * funnel to construct a PowDB client instead of a `pg` client for `powdb://`.
68
68
  */
69
+ import { type TurbineLogger } from './logger.js';
69
70
  import { type PowdbCapabilities } from './powdb-shared.js';
70
71
  import type { SchemaMetadata } from './schema.js';
71
72
  /** A minimal rows-returning executor over a PowDB connection (embedded or networked). */
@@ -86,6 +87,8 @@ export interface PowdbIntrospectOptions {
86
87
  * to run ungated; the pool paths that know the version pass it through.
87
88
  */
88
89
  capabilities?: PowdbCapabilities;
90
+ /** Where a table without a usable primary key is reported. Defaults to the console logger. */
91
+ logger?: TurbineLogger;
89
92
  }
90
93
  /**
91
94
  * Read a live PowDB database into {@link SchemaMetadata} via `schema` +
@@ -13,8 +13,8 @@
13
13
  * shape the SQL introspectors produce, so a code-first PowDB database can be
14
14
  * introspected for bootstrap/verification. It is transport-agnostic: the caller
15
15
  * supplies an `exec(powql)` that returns row objects **keyed by column name**.
16
- * - Embedded / owned pool: `exec = async (q) => ({ rows: await db.raw([q]) })`
17
- * using a live `turbinePowDB` client's `raw` tagged template.
16
+ * - Embedded / owned pool: `exec = async (q) => ({ rows: await db.rawUnsafe(q) })`
17
+ * using a live `turbinePowDB` client.
18
18
  * - Networked: the raw `@zvndev/powdb-client` returns POSITIONAL rows
19
19
  * (`{ columns: string[], rows: string[][] }`), so zip them into records.
20
20
  * A bare `(await client.query(q)).rows` would hand this function `string[][]`
@@ -67,7 +67,8 @@
67
67
  * funnel to construct a PowDB client instead of a `pg` client for `powdb://`.
68
68
  */
69
69
  import { ValidationError } from './errors.js';
70
- import { applyTableFilters } from './introspect.js';
70
+ import { applyTableFilters, assertDistinctColumnFields, nullRecord, printableCatalogName } from './introspect.js';
71
+ import { consoleLogger, logTo } from './logger.js';
71
72
  import { quotePowqlIdent, requireCapability } from './powdb-shared.js';
72
73
  import { singularize, snakeToCamel } from './schema.js';
73
74
  /** Coerce a wire cell to string (legacy wire cells are strings; native cells may be typed). */
@@ -133,7 +134,9 @@ export async function introspectPowdbDatabase(exec, options = {}) {
133
134
  }
134
135
  // include / exclude + default bookkeeping-table exclusions (F12).
135
136
  const tableNames = applyTableFilters(candidateTables, options);
136
- const tables = {};
137
+ // Keyed by catalog names, so prototype-less: a table or column named
138
+ // `__proto__`, `constructor` or `toString` is data, never a prototype member.
139
+ const tables = nullRecord();
137
140
  for (const tableName of tableNames) {
138
141
  // `describe` needs the table name in bare-identifier position → quote it so
139
142
  // a reserved-word / non-bare table name (`order`) does not become a parse
@@ -154,11 +157,11 @@ export async function introspectPowdbDatabase(exec, options = {}) {
154
157
  index: asString(r.index),
155
158
  }));
156
159
  const columns = [];
157
- const columnMap = {};
158
- const reverseColumnMap = {};
160
+ const columnMap = nullRecord();
161
+ const reverseColumnMap = nullRecord();
159
162
  const dateColumns = new Set();
160
- const dialectTypes = {};
161
- const pgTypes = {};
163
+ const dialectTypes = nullRecord();
164
+ const pgTypes = nullRecord();
162
165
  const allColumns = [];
163
166
  const uniqueColumns = [];
164
167
  const indexes = [];
@@ -206,15 +209,17 @@ export async function introspectPowdbDatabase(exec, options = {}) {
206
209
  });
207
210
  }
208
211
  }
212
+ // Two columns resolving to one field would fold into one property on read.
213
+ assertDistinctColumnFields(tableName, columns);
209
214
  // Primary key: first non-nullable unique column, preferring one named `id`.
210
215
  let primaryKey = [];
211
216
  if (pkCandidates.length > 0) {
212
217
  primaryKey = [pkCandidates.includes('id') ? 'id' : pkCandidates[0]];
213
218
  }
214
219
  else {
215
- console.warn(`[turbine] PowDB introspection: table "${tableName}" has no non-nullable unique column; ` +
220
+ logTo(options.logger ?? consoleLogger, 'warn', `PowDB introspection: table "${printableCatalogName(tableName)}" has no non-nullable unique column; ` +
216
221
  'primaryKey is [] (single-row operations will fail). Supply a primary key via code-first ' +
217
- '`defineSchema` metadata if this table needs findUnique/update/delete by id.');
222
+ '`defineSchema` metadata if this table needs findUnique/update/delete by id.', { code: 'powdb-introspect-no-primary-key', table: tableName });
218
223
  }
219
224
  tables[tableName] = {
220
225
  name: tableName,
@@ -230,7 +235,7 @@ export async function introspectPowdbDatabase(exec, options = {}) {
230
235
  // Populated below from `schema links` when the engine supports link
231
236
  // introspection (>= 0.19.1); stays `{}` otherwise (PowDB has no declared
232
237
  // foreign keys, so pre-link introspection reports no relations).
233
- relations: {},
238
+ relations: nullRecord(),
234
239
  indexes,
235
240
  };
236
241
  }
@@ -240,7 +245,7 @@ export async function introspectPowdbDatabase(exec, options = {}) {
240
245
  if (options.capabilities?.linkIntrospection) {
241
246
  await introspectPowdbLinks(exec, tables);
242
247
  }
243
- return { tables, enums: {} };
248
+ return { tables, enums: nullRecord() };
244
249
  }
245
250
  /**
246
251
  * Read `schema links` and populate {@link TableMetadata.relations}: the first
@@ -275,8 +280,8 @@ async function introspectPowdbLinks(exec, tables) {
275
280
  cardinality: asString(r.cardinality),
276
281
  }));
277
282
  for (const link of linkRows) {
278
- const owner = tables[link.owner];
279
- const target = tables[link.target];
283
+ const owner = Object.hasOwn(tables, link.owner) ? tables[link.owner] : undefined;
284
+ const target = Object.hasOwn(tables, link.target) ? tables[link.target] : undefined;
280
285
  // A link referencing a filtered-out (include/exclude) table is skipped: we
281
286
  // cannot describe the target, so the relation would be unusable.
282
287
  if (!owner || !target)
@@ -321,13 +326,13 @@ async function introspectPowdbLinks(exec, tables) {
321
326
  }
322
327
  /** True when `name` already names a relation OR a column field/name on `meta`. */
323
328
  function relationNameTaken(meta, name) {
324
- if (meta.relations[name])
329
+ if (Object.hasOwn(meta.relations, name))
325
330
  return true;
326
331
  return meta.columns.some((c) => c.field === name || c.name === name);
327
332
  }
328
333
  /** Add a relation to a table's relations map (keyed by its camelCase name). */
329
334
  function addRelation(meta, rel) {
330
- if (meta.relations[rel.name])
335
+ if (Object.hasOwn(meta.relations, rel.name))
331
336
  return; // never clobber an existing relation
332
337
  meta.relations[rel.name] = rel;
333
338
  }
@@ -233,15 +233,36 @@ export type PowqlType = 'str' | 'int' | 'float' | 'bool' | 'json';
233
233
  * asserts the equivalence over a corpus rather than asserting my reading of it.
234
234
  */
235
235
  export declare function baseTsType(tsType: string): string;
236
+ /** A schema's enum types (`SchemaMetadata.enums`): type name to labels, in declaration order. */
237
+ export type PowdbEnums = Readonly<Record<string, readonly string[]>>;
236
238
  /**
237
- * Does this column map to PowDB's native `json` document type? A Postgres
239
+ * The labels of the enum type this column is declared as, or `undefined` when
240
+ * it is not an enum column of `enums`.
241
+ *
242
+ * An enum column carries its TYPE NAME as `dialectType` / `pgType` and a
243
+ * `tsType` that says nothing useful (`unknown` from `defineSchema`, a union of
244
+ * the labels from introspection), so the column's own metadata cannot answer
245
+ * the question: the schema's `enums` map is the only authority. PowDB has no
246
+ * enum type, so such a column is a `str` holding the label text, which is what
247
+ * PostgreSQL stores and returns. Without this the tsType heuristic in
248
+ * {@link isJsonColumn} declared a `defineSchema` enum column as `json`, and
249
+ * writing a label failed as invalid JSON.
250
+ */
251
+ export declare function powdbEnumLabels(col: ColumnMetadata, enums: PowdbEnums | undefined): readonly string[] | undefined;
252
+ /**
253
+ * Does this column map to PowDB's native `json` document type? An enum column
254
+ * of `enums` never does (see {@link powdbEnumLabels}). A Postgres
238
255
  * `json`/`jsonb` type (via `dialectType`/`pgType`) is authoritative; otherwise
239
256
  * the tsType heuristic (`Record<…>`, `object`, `unknown`, an object/array
240
257
  * literal) that the four scalar branches do not claim. Array columns never map
241
258
  * to json, a PowDB array only exists INSIDE a json document, so a Postgres
242
259
  * array column has no PowDB shape and still throws in {@link powqlColumnType}.
260
+ *
261
+ * `enums` is optional for callers outside Turbine; every call inside the
262
+ * engine passes the schema's map (a source scan in fx-powdb-r2.test.ts
263
+ * checks it).
243
264
  */
244
- export declare function isJsonColumn(col: ColumnMetadata): boolean;
265
+ export declare function isJsonColumn(col: ColumnMetadata, enums?: PowdbEnums): boolean;
245
266
  /**
246
267
  * Map a Turbine column to the PowQL DDL type used in `defineSchema` →
247
268
  * `type T { … }`. Turbine never emits PowDB's `uuid`/`datetime`/`bytes` types,
@@ -249,10 +270,20 @@ export declare function isJsonColumn(col: ColumnMetadata): boolean;
249
270
  * - `Date` → `int` (epoch micros) - `boolean` → `bool`
250
271
  * - integral `number`/`bigint` → `int` - fractional `number` → `float`
251
272
  * - JSON / object columns → `json` (native PowDB document type, ≥ 0.12)
273
+ * - an enum column of `enums` → `str` (the label text)
252
274
  * - everything else (incl. UUID/PK strings) → `str`
253
275
  * Array (non-json) and bytes columns throw, they have no PowDB equivalent.
254
276
  */
255
- export declare function powqlColumnType(col: ColumnMetadata): PowqlType;
277
+ export declare function powqlColumnType(col: ColumnMetadata, enums?: PowdbEnums): PowqlType;
278
+ /**
279
+ * Is this a `numeric` / `decimal` / `money` column that PowDB stores as `str`?
280
+ *
281
+ * A Postgres-sourced decimal surfaces as a JS string (so its digits survive),
282
+ * and PowDB has no decimal type, so the column is DDL'd as PowQL `str`. Every
283
+ * comparison, ordering and aggregate the engine runs on it is then a TEXT
284
+ * operation, which is why the query generator refuses those on such a column.
285
+ */
286
+ export declare function isPowdbDecimalTextColumn(col: ColumnMetadata, enums?: PowdbEnums): boolean;
256
287
  /**
257
288
  * Is a column stored as `int` epoch micros but surfaced as a JS `Date`?
258
289
  *
@@ -325,7 +356,7 @@ export declare function quotePowqlDotted(name: string): string;
325
356
  * Every PowDB value arrives as a string; NULL arrives as the bareword `"null"`.
326
357
  * Metadata resolves the `"null"` ambiguity for nullable non-string columns.
327
358
  */
328
- export declare function coerceValue(raw: string, col: ColumnMetadata): unknown;
359
+ export declare function coerceValue(raw: string, col: ColumnMetadata, enums?: PowdbEnums): unknown;
329
360
  /**
330
361
  * Coerce a single cell that arrived over the NATIVE typed wire (decoded from a
331
362
  * {@link PowdbWireValue}, so already a JS `bigint`/`number`/`boolean`/`string`/
@@ -354,7 +385,7 @@ export declare function coerceNativeValue(value: unknown, col: ColumnMetadata):
354
385
  * cells arrive pre-typed and go through {@link coerceNativeValue} (see F3).
355
386
  * Callers on the native transport pass `this.pool.capabilities.nativeRaw`.
356
387
  */
357
- export declare function rowToEntity(raw: Record<string, unknown>, meta: TableMetadata, native?: boolean): Record<string, unknown>;
388
+ export declare function rowToEntity(raw: Record<string, unknown>, meta: TableMetadata, native?: boolean, enums?: PowdbEnums): Record<string, unknown>;
358
389
  /**
359
390
  * True when `err` is the stale-wire-frame {@link ConnectionError} produced by
360
391
  * {@link wrapPowdbError} (its `.cause` is a `protocol_error` PowDBError, or the