turbine-orm 0.79.1 → 0.80.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 (71) hide show
  1. package/README.md +4 -4
  2. package/dist/checkout.d.ts +53 -0
  3. package/dist/checkout.js +78 -0
  4. package/dist/cjs/checkout.d.ts +53 -0
  5. package/dist/cjs/checkout.js +82 -0
  6. package/dist/cjs/cli/index.js +4 -0
  7. package/dist/cjs/cli/mcp.js +4 -0
  8. package/dist/cjs/cli/migrate.js +6 -0
  9. package/dist/cjs/cli/observe.js +8 -0
  10. package/dist/cjs/cli/studio.js +13 -1
  11. package/dist/cjs/client.d.ts +12 -2
  12. package/dist/cjs/client.js +61 -75
  13. package/dist/cjs/connection-guard.d.ts +120 -0
  14. package/dist/cjs/connection-guard.js +191 -0
  15. package/dist/cjs/errors.d.ts +26 -0
  16. package/dist/cjs/errors.js +85 -1
  17. package/dist/cjs/index.d.ts +1 -1
  18. package/dist/cjs/nested-write.d.ts +12 -2
  19. package/dist/cjs/nested-write.js +4 -10
  20. package/dist/cjs/pipeline.js +12 -7
  21. package/dist/cjs/plan-flip-probe.js +4 -0
  22. package/dist/cjs/powdb-shared.d.ts +22 -2
  23. package/dist/cjs/powdb-shared.js +27 -2
  24. package/dist/cjs/powdb.js +36 -37
  25. package/dist/cjs/powql.d.ts +51 -6
  26. package/dist/cjs/powql.js +199 -45
  27. package/dist/cjs/prisma-compat.js +28 -4
  28. package/dist/cjs/query/builder.d.ts +44 -24
  29. package/dist/cjs/query/builder.js +125 -66
  30. package/dist/cjs/query/deferred.d.ts +9 -0
  31. package/dist/cjs/query/option-surface.js +12 -0
  32. package/dist/cjs/query/types.d.ts +68 -4
  33. package/dist/cjs/query/writes.d.ts +39 -9
  34. package/dist/cjs/query/writes.js +72 -34
  35. package/dist/cjs/realtime.d.ts +46 -2
  36. package/dist/cjs/realtime.js +125 -20
  37. package/dist/cjs/schema-sql.js +6 -0
  38. package/dist/cli/index.js +4 -0
  39. package/dist/cli/mcp.js +4 -0
  40. package/dist/cli/migrate.js +6 -0
  41. package/dist/cli/observe.js +8 -0
  42. package/dist/cli/studio.js +13 -1
  43. package/dist/client.d.ts +12 -2
  44. package/dist/client.js +62 -76
  45. package/dist/connection-guard.d.ts +120 -0
  46. package/dist/connection-guard.js +183 -0
  47. package/dist/errors.d.ts +26 -0
  48. package/dist/errors.js +83 -1
  49. package/dist/index.d.ts +1 -1
  50. package/dist/index.js +1 -1
  51. package/dist/nested-write.d.ts +12 -2
  52. package/dist/nested-write.js +4 -10
  53. package/dist/pipeline.js +13 -8
  54. package/dist/plan-flip-probe.js +4 -0
  55. package/dist/powdb-shared.d.ts +22 -2
  56. package/dist/powdb-shared.js +25 -2
  57. package/dist/powdb.js +23 -24
  58. package/dist/powql.d.ts +51 -6
  59. package/dist/powql.js +200 -46
  60. package/dist/prisma-compat.js +28 -4
  61. package/dist/query/builder.d.ts +44 -24
  62. package/dist/query/builder.js +126 -67
  63. package/dist/query/deferred.d.ts +9 -0
  64. package/dist/query/option-surface.js +12 -0
  65. package/dist/query/types.d.ts +68 -4
  66. package/dist/query/writes.d.ts +39 -9
  67. package/dist/query/writes.js +71 -34
  68. package/dist/realtime.d.ts +46 -2
  69. package/dist/realtime.js +125 -20
  70. package/dist/schema-sql.js +6 -0
  71. package/package.json +5 -3
@@ -10,8 +10,10 @@
10
10
  * Schema-driven: all column names, types, and relations come from introspected
11
11
  * metadata, nothing is hardcoded.
12
12
  */
13
+ import { acquireConnection, openCheckout } from '../checkout.js';
14
+ import { settleEventLoop } from '../connection-guard.js';
13
15
  import { postgresDialect } from '../dialect.js';
14
- import { NotFoundError, TimeoutError, UnsupportedFeatureError, ValidationError, wrapPgError } from '../errors.js';
16
+ import { explainConnectionLoss, isStaleConnectionError, NotFoundError, TimeoutError, UnsupportedFeatureError, ValidationError, wrapPgError, } from '../errors.js';
15
17
  import { missingIndexForRelation, schemaHasIndexInfo } from '../index-advisor.js';
16
18
  import { executeNestedCreate, executeNestedUpdate, hasRelationFields, } from '../nested-write.js';
17
19
  import { normalizeKeyColumns, snakeToCamel } from '../schema.js';
@@ -1677,7 +1679,7 @@ export class QueryInterface {
1677
1679
  const deferred = single
1678
1680
  ? this.buildFindUnique(baseArgs)
1679
1681
  : this.buildFindMany(baseArgs);
1680
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
1682
+ const result = await this.readWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
1681
1683
  const rows = deferred.transform(result);
1682
1684
  const entities = single ? (rows ? [rows] : []) : rows;
1683
1685
  // Unconditionally, even for zero rows: with no parents the loader is a
@@ -1718,7 +1720,7 @@ export class QueryInterface {
1718
1720
  // too: a batched load re-issues the SAME tenant-shaped predicate one
1719
1721
  // level down, so leaving those named would keep exactly the plan-cache
1720
1722
  // exposure the caller asked to be rid of.
1721
- exec: (sql, params, preparedName) => this.queryWithTimeout(sql, params, timeout, this.preparedNameFor({ forceCustomPlan }, preparedName)),
1723
+ exec: (sql, params, preparedName) => this.readWithTimeout(sql, params, timeout, this.preparedNameFor({ forceCustomPlan }, preparedName)),
1722
1724
  quote: (name) => this.q(name),
1723
1725
  buildInClause: (expr, paramRef, negated) => this.inClause(expr, paramRef, negated),
1724
1726
  inClauseParam: (values) => this.inParam(values),
@@ -1831,7 +1833,7 @@ export class QueryInterface {
1831
1833
  const { baseArgs, strip } = this.prepareBatchedBase(args, withClause);
1832
1834
  // baseArgs.with is always undefined here; the cast just bridges the R generic.
1833
1835
  const deferred = this.buildFindMany(baseArgs);
1834
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
1836
+ const result = await this.readWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
1835
1837
  const entities = deferred.transform(result);
1836
1838
  // Unconditionally, even for zero rows: see the 'auto' path above, the
1837
1839
  // loader doubles as the compile-time validation walk of the `with` tree.
@@ -1994,7 +1996,7 @@ export class QueryInterface {
1994
1996
  resetUnlimitedWarnings() {
1995
1997
  this.warnedTables.clear();
1996
1998
  }
1997
- emitQueryEvent(sql, params, duration, action, rows, error) {
1999
+ emitQueryEvent(sql, params, duration, action, rows, error, retried) {
1998
2000
  const onQuery = this.options?._onQuery;
1999
2001
  if (!onQuery)
2000
2002
  return;
@@ -2009,6 +2011,7 @@ export class QueryInterface {
2009
2011
  timestamp: new Date(),
2010
2012
  error,
2011
2013
  strategy: this.currentStrategyTag,
2014
+ ...(retried ? { retried: true } : {}),
2012
2015
  });
2013
2016
  }
2014
2017
  catch {
@@ -2078,13 +2081,39 @@ export class QueryInterface {
2078
2081
  }
2079
2082
  return undefined;
2080
2083
  }
2084
+ /**
2085
+ * {@link queryWithTimeout} for a statement that only READS, which makes it
2086
+ * safe to send twice. Outside a transaction, a read that fails because the
2087
+ * connection it went out on had already been closed by the server
2088
+ * ({@link isStaleConnectionError}) is sent once more, after one event-loop
2089
+ * turn, on whatever connection the pool hands out next.
2090
+ *
2091
+ * Reads only, and the distinction is the whole design. For a write, "the
2092
+ * connection died" does not say whether the statement committed first: a
2093
+ * connection can drop after the server commits and before the reply
2094
+ * arrives, so resending an INSERT can insert it twice. A read has no such
2095
+ * outcome. Nor inside a transaction, where the connection that died WAS the
2096
+ * transaction and a retry on another one would run outside it.
2097
+ *
2098
+ * The loop turn is what makes a second attempt worth making: pg-pool evicts
2099
+ * an idle connection whose close it has read, so after one poll phase every
2100
+ * other connection that died alongside this one is gone from the idle list
2101
+ * rather than lent out to the retry.
2102
+ *
2103
+ * Every read call site uses this and every write uses `queryWithTimeout`
2104
+ * directly; `src/test/stale-connection-retry.test.ts` pins which is which.
2105
+ */
2106
+ readWithTimeout(sql, params, timeout, preparedName) {
2107
+ return this.queryWithTimeout(sql, params, timeout, preparedName, true);
2108
+ }
2081
2109
  /**
2082
2110
  * Execute a pool.query with an optional timeout.
2083
2111
  * If timeout is set, races the query against a timer and rejects on expiry.
2084
2112
  * pg driver errors are translated to typed Turbine errors via wrapPgError.
2113
+ * `retryIfStale` is {@link readWithTimeout}'s; nothing else sets it.
2085
2114
  */
2086
- async queryWithTimeout(sql, params, timeout, preparedName) {
2087
- const start = performance.now();
2115
+ async queryWithTimeout(sql, params, timeout, preparedName, retryIfStale = false) {
2116
+ let start = performance.now();
2088
2117
  const action = this.currentAction;
2089
2118
  // Build the query argument, use object form with `name` for prepared
2090
2119
  // statements, or the plain (text, values) form otherwise.
@@ -2094,13 +2123,33 @@ export class QueryInterface {
2094
2123
  // cast. Guarded at runtime by `preparedStatementsEnabled`, which only the
2095
2124
  // drivers that accept it turn on.
2096
2125
  const usePrepared = preparedName && this.preparedStatementsEnabled;
2097
- const exec = usePrepared
2126
+ const send = () => usePrepared
2098
2127
  ? this.pool.query({
2099
2128
  name: preparedName,
2100
2129
  text: sql,
2101
2130
  values: params,
2102
2131
  })
2103
2132
  : this.pool.query(sql, params);
2133
+ let exec = send();
2134
+ // Set when the caller's timeout has already answered, so a first attempt
2135
+ // that fails AFTER that does not send a retry nobody is waiting for.
2136
+ let abandoned = false;
2137
+ if (retryIfStale && !this.txScoped) {
2138
+ exec = exec.catch(async (err) => {
2139
+ if (abandoned || !isStaleConnectionError(err))
2140
+ throw err;
2141
+ // The failed attempt is reported as its own event, marked `retried`,
2142
+ // so a listener counting errors sees it and one alerting on failed
2143
+ // CALLS can skip it. The final event below times the retry alone.
2144
+ const wrapped = wrapPgError(err);
2145
+ this.emitQueryEvent(sql, params, performance.now() - start, action, 0, wrapped instanceof Error ? wrapped : undefined, true);
2146
+ await settleEventLoop();
2147
+ if (abandoned)
2148
+ throw err;
2149
+ start = performance.now();
2150
+ return send();
2151
+ });
2152
+ }
2104
2153
  if (!timeout) {
2105
2154
  try {
2106
2155
  const result = await exec;
@@ -2115,7 +2164,10 @@ export class QueryInterface {
2115
2164
  }
2116
2165
  let timer;
2117
2166
  const timeoutPromise = new Promise((_, reject) => {
2118
- timer = setTimeout(() => reject(new TimeoutError(timeout)), timeout);
2167
+ timer = setTimeout(() => {
2168
+ abandoned = true;
2169
+ reject(new TimeoutError(timeout));
2170
+ }, timeout);
2119
2171
  });
2120
2172
  try {
2121
2173
  const result = await Promise.race([exec, timeoutPromise]);
@@ -2157,19 +2209,37 @@ export class QueryInterface {
2157
2209
  // Write compilation (extracted to writes.ts).
2158
2210
  // ---------------------------------------------------------------------------
2159
2211
  buildCreate(args) {
2160
- return writesMod.buildCreate(this.ctx, args);
2212
+ return writesMod.buildCreate(this.ctx, args, this.resolveWriteProjection(args));
2213
+ }
2214
+ /**
2215
+ * A single-row write's `select` / `omit`, resolved through the SAME
2216
+ * `resolveProjection` reads use (see WriteProjection in writes.ts), or
2217
+ * `undefined` for the default return shape. Resolved here rather than in
2218
+ * writes.ts because relations.ts imports writes.ts.
2219
+ */
2220
+ resolveWriteProjection(args) {
2221
+ if (args.select === undefined && args.omit === undefined)
2222
+ return undefined;
2223
+ const columns = relationsMod.resolveProjection(this.ctx, this.table, this.tableMeta, args.select, args.omit, false);
2224
+ if (!columns || columns.length === 0) {
2225
+ // Only an `omit` naming every column gets here (an empty `select` is
2226
+ // refused inside resolveProjection): a write must return SOMETHING, and
2227
+ // an empty RETURNING list is a syntax error on every engine.
2228
+ throw new ValidationError(`\`omit\` on a write to "${this.table}" leaves no column to return. Name the fields to keep with \`select\` instead.`);
2229
+ }
2230
+ return { columns };
2161
2231
  }
2162
2232
  buildCreateMany(args) {
2163
2233
  return writesMod.buildCreateMany(this.ctx, args);
2164
2234
  }
2165
2235
  buildUpdate(args) {
2166
- return writesMod.buildUpdate(this.ctx, args);
2236
+ return writesMod.buildUpdate(this.ctx, args, this.resolveWriteProjection(args));
2167
2237
  }
2168
2238
  buildDelete(args) {
2169
- return writesMod.buildDelete(this.ctx, args);
2239
+ return writesMod.buildDelete(this.ctx, args, this.resolveWriteProjection(args));
2170
2240
  }
2171
2241
  buildUpsert(args) {
2172
- return writesMod.buildUpsert(this.ctx, args);
2242
+ return writesMod.buildUpsert(this.ctx, args, this.resolveWriteProjection(args));
2173
2243
  }
2174
2244
  buildUpdateMany(args) {
2175
2245
  return writesMod.buildUpdateMany(this.ctx, args);
@@ -2250,7 +2320,7 @@ export class QueryInterface {
2250
2320
  }
2251
2321
  }
2252
2322
  const deferred = this.buildFindUnique(args);
2253
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
2323
+ const result = await this.readWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
2254
2324
  return deferred.transform(result);
2255
2325
  });
2256
2326
  }
@@ -2274,7 +2344,7 @@ export class QueryInterface {
2274
2344
  const proj = includeKeysForBatching(this.tableMeta, args.select, args.omit, needed, defaultProjectionFields(this.tableMeta, resolveUnsafeFlag(args.includePii, 'includePii')));
2275
2345
  const baseArgs = { ...args, with: undefined, select: proj.select, omit: proj.omit };
2276
2346
  const deferred = this.buildFindUnique(baseArgs);
2277
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
2347
+ const result = await this.readWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
2278
2348
  const entity = deferred.transform(result);
2279
2349
  // A miss still walks the `with` tree (compile-only child builds), so the
2280
2350
  // same args throw or pass identically whether or not the row exists,
@@ -2508,7 +2578,7 @@ export class QueryInterface {
2508
2578
  }
2509
2579
  }
2510
2580
  const deferred = this.buildFindMany(args);
2511
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args?.timeout, this.preparedNameFor(args, deferred.preparedName));
2581
+ const result = await this.readWithTimeout(deferred.sql, deferred.params, args?.timeout, this.preparedNameFor(args, deferred.preparedName));
2512
2582
  return deferred.transform(result);
2513
2583
  });
2514
2584
  }
@@ -3146,7 +3216,7 @@ export class QueryInterface {
3146
3216
  ...args,
3147
3217
  limit: batchSize + 1,
3148
3218
  });
3149
- const speculativeResult = await this.queryWithTimeout(speculativeDeferred.sql, speculativeDeferred.params, args?.timeout, streamPreparedName);
3219
+ const speculativeResult = await this.readWithTimeout(speculativeDeferred.sql, speculativeDeferred.params, args?.timeout, streamPreparedName);
3150
3220
  if (speculativeResult.rows.length <= batchSize) {
3151
3221
  // Small drain, hand over the whole result and return, no cursor needed.
3152
3222
  if (speculativeResult.rows.length > 0)
@@ -3166,7 +3236,8 @@ export class QueryInterface {
3166
3236
  // connection, so `pool.query` already IS that connection. The stream rides
3167
3237
  // on it, is never released here, and the dialect is told to emit no
3168
3238
  // transaction control of its own (`ambientTransaction`).
3169
- const client = this.txScoped ? null : await this.acquireConnection();
3239
+ const held = this.txScoped ? null : await this.acquireConnection();
3240
+ const client = held?.client ?? null;
3170
3241
  const conn = client ?? {
3171
3242
  query: async (text, values) => (await this.pool.query(text, values)),
3172
3243
  };
@@ -3177,10 +3248,10 @@ export class QueryInterface {
3177
3248
  }
3178
3249
  catch (err) {
3179
3250
  // Wrap pg constraint errors so streaming surfaces typed errors like the rest of the API
3180
- throw wrapPgError(err);
3251
+ throw explainConnectionLoss(wrapPgError(err), held?.checkout.lostWith);
3181
3252
  }
3182
3253
  finally {
3183
- client?.release();
3254
+ held?.checkout.release();
3184
3255
  }
3185
3256
  }
3186
3257
  /**
@@ -3305,7 +3376,7 @@ export class QueryInterface {
3305
3376
  }
3306
3377
  }
3307
3378
  const deferred = this.buildFindFirst(args);
3308
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args?.timeout, this.preparedNameFor(args, deferred.preparedName));
3379
+ const result = await this.readWithTimeout(deferred.sql, deferred.params, args?.timeout, this.preparedNameFor(args, deferred.preparedName));
3309
3380
  return deferred.transform(result);
3310
3381
  });
3311
3382
  }
@@ -3329,7 +3400,7 @@ export class QueryInterface {
3329
3400
  async findFirstOrThrow(args) {
3330
3401
  return this.executeWithMiddleware('findFirstOrThrow', (args ?? {}), async () => {
3331
3402
  const deferred = this.buildFindFirstOrThrow(args);
3332
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args?.timeout, this.preparedNameFor(args, deferred.preparedName));
3403
+ const result = await this.readWithTimeout(deferred.sql, deferred.params, args?.timeout, this.preparedNameFor(args, deferred.preparedName));
3333
3404
  return deferred.transform(result);
3334
3405
  });
3335
3406
  }
@@ -3358,7 +3429,7 @@ export class QueryInterface {
3358
3429
  async findUniqueOrThrow(args) {
3359
3430
  return this.executeWithMiddleware('findUniqueOrThrow', args, async () => {
3360
3431
  const deferred = this.buildFindUniqueOrThrow(args);
3361
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
3432
+ const result = await this.readWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
3362
3433
  return deferred.transform(result);
3363
3434
  });
3364
3435
  }
@@ -3420,58 +3491,50 @@ export class QueryInterface {
3420
3491
  // -------------------------------------------------------------------------
3421
3492
  async nestedCreate(args) {
3422
3493
  const data = args.data;
3494
+ // Validated up front, before any row is written, and then applied by the
3495
+ // nested engine's final read-back, which already speaks select/omit.
3496
+ this.resolveWriteProjection(args);
3497
+ const shape = { select: args.select, omit: args.omit };
3423
3498
  if (this.txScoped) {
3424
3499
  const ctx = this.buildNestedCtx();
3425
- return executeNestedCreate(ctx, this.table, data);
3500
+ return executeNestedCreate(ctx, this.table, data, 0, [], shape);
3426
3501
  }
3427
3502
  return this.runInImplicitTx(async (ctx) => {
3428
- const result = await executeNestedCreate(ctx, this.table, data);
3503
+ const result = await executeNestedCreate(ctx, this.table, data, 0, [], shape);
3429
3504
  return result;
3430
3505
  });
3431
3506
  }
3432
3507
  async nestedUpdate(args) {
3433
3508
  const data = args.data;
3434
3509
  const where = args.where;
3510
+ this.resolveWriteProjection(args);
3511
+ const shape = { select: args.select, omit: args.omit };
3435
3512
  if (this.txScoped) {
3436
3513
  const ctx = this.buildNestedCtx();
3437
- return executeNestedUpdate(ctx, this.table, where, data);
3514
+ return executeNestedUpdate(ctx, this.table, where, data, 0, [], shape);
3438
3515
  }
3439
3516
  return this.runInImplicitTx(async (ctx) => {
3440
- const result = await executeNestedUpdate(ctx, this.table, where, data);
3517
+ const result = await executeNestedUpdate(ctx, this.table, where, data, 0, [], shape);
3441
3518
  return result;
3442
3519
  });
3443
3520
  }
3444
3521
  /**
3445
- * Check out a pooled connection, translating a driver failure into a typed
3446
- * Turbine error.
3447
- *
3448
- * `pool.connect()` is where the first-run failures land: wrong password
3449
- * (SQLSTATE 28P01), no such database (3D000), nothing listening
3450
- * (ECONNREFUSED), an unverifiable TLS certificate. Unwrapped, every one of
3451
- * those surfaces from an ordinary `db.users.create({ data: { ...nested } })`
3452
- * as a raw pg `DatabaseError` whose `.code` is a SQLSTATE, on the same
3453
- * property Turbine puts `TURBINE_E0NN` in.
3454
- *
3455
- * client.ts has its own copy for `$transaction` / `connect()`; this one
3456
- * exists because `query/` must not import client.ts (circular dependency).
3457
- * The query paths need no equivalent: `pool.query()` opens the connection
3458
- * itself and rejects with the connect error, which the query boundary
3459
- * already wraps.
3522
+ * Check out a guarded pooled connection, as a typed error when that fails.
3523
+ * Shared with client.ts through checkout.ts (see there for why each part
3524
+ * matters); the cursor stream holds it, and a nested write opens its
3525
+ * implicit transaction on one through `openCheckout`.
3460
3526
  */
3461
- async acquireConnection() {
3462
- try {
3463
- return await this.pool.connect();
3464
- }
3465
- catch (err) {
3466
- throw wrapPgError(err);
3467
- }
3527
+ acquireConnection() {
3528
+ return acquireConnection(this.pool);
3468
3529
  }
3469
3530
  async runInImplicitTx(fn) {
3470
- const client = await this.acquireConnection();
3471
- let began = false;
3531
+ // BEGIN runs inside openCheckout, which sends it once more on a fresh
3532
+ // connection when the first one turns out to be dead and releases the
3533
+ // connection itself when BEGIN fails for good. The catch below therefore
3534
+ // only sees a transaction that began, and never emits a stray ROLLBACK on
3535
+ // a connection that opened none.
3536
+ const { client, checkout } = await openCheckout(this.pool, (c) => c.query(this.dialect.beginStatement()));
3472
3537
  try {
3473
- await client.query(this.dialect.beginStatement());
3474
- began = true;
3475
3538
  const { TransactionClient } = await import('../client.js');
3476
3539
  const tx = new TransactionClient(
3477
3540
  // biome-ignore lint/suspicious/noExplicitAny: MiddlewareFn and Middleware are structurally identical
@@ -3495,20 +3558,16 @@ export class QueryInterface {
3495
3558
  return result;
3496
3559
  }
3497
3560
  catch (err) {
3498
- // Only roll back a transaction we actually opened: a failed BEGIN must
3499
- // not emit a stray ROLLBACK on a connection that never began one.
3500
- if (began) {
3501
- try {
3502
- await client.query(this.dialect.rollbackStatement());
3503
- }
3504
- catch {
3505
- // Best-effort rollback: connection may have died.
3506
- }
3561
+ try {
3562
+ await client.query(this.dialect.rollbackStatement());
3563
+ }
3564
+ catch {
3565
+ // Best-effort rollback: connection may have died.
3507
3566
  }
3508
- throw err;
3567
+ throw explainConnectionLoss(err, checkout.lostWith);
3509
3568
  }
3510
3569
  finally {
3511
- client.release();
3570
+ checkout.release();
3512
3571
  }
3513
3572
  }
3514
3573
  buildNestedCtx() {
@@ -3571,7 +3630,7 @@ export class QueryInterface {
3571
3630
  async count(args) {
3572
3631
  return this.executeWithMiddleware('count', (args ?? {}), async () => {
3573
3632
  const deferred = this.buildCount(args);
3574
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args?.timeout, this.preparedNameFor(args, deferred.preparedName));
3633
+ const result = await this.readWithTimeout(deferred.sql, deferred.params, args?.timeout, this.preparedNameFor(args, deferred.preparedName));
3575
3634
  return deferred.transform(result);
3576
3635
  });
3577
3636
  }
@@ -3615,7 +3674,7 @@ export class QueryInterface {
3615
3674
  async groupBy(args) {
3616
3675
  return this.executeWithMiddleware('groupBy', args, async () => {
3617
3676
  const deferred = this.buildGroupBy(args);
3618
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
3677
+ const result = await this.readWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
3619
3678
  return deferred.transform(result);
3620
3679
  });
3621
3680
  }
@@ -3634,7 +3693,7 @@ export class QueryInterface {
3634
3693
  async aggregate(args) {
3635
3694
  return this.executeWithMiddleware('aggregate', args, async () => {
3636
3695
  const deferred = this.buildAggregate(args);
3637
- const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
3696
+ const result = await this.readWithTimeout(deferred.sql, deferred.params, args.timeout, this.preparedNameFor(args, deferred.preparedName));
3638
3697
  return deferred.transform(result);
3639
3698
  });
3640
3699
  }
@@ -108,6 +108,15 @@ export interface QueryEvent {
108
108
  * batch statements are timed individually. Absent for everything else.
109
109
  */
110
110
  batch?: 'pipeline' | 'transaction';
111
+ /**
112
+ * Set on a READ that failed because its connection had already been closed
113
+ * by the server, and that Turbine therefore sent once more on a fresh one.
114
+ * This event carries that first attempt's `error`; the retry reports its own
115
+ * event, so a failed read that was then answered shows as two events and a
116
+ * listener alerting on failed calls can skip the ones marked here. Writes
117
+ * and statements inside a transaction are never retried.
118
+ */
119
+ retried?: true;
111
120
  }
112
121
  export type QueryEventListener = (event: QueryEvent) => void;
113
122
  /** Options passed from TurbineClient to QueryInterface */
@@ -114,6 +114,9 @@ export const FIND_MANY_STREAM_OPTIONS = {
114
114
  };
115
115
  export const CREATE_OPTIONS = {
116
116
  data: 'prisma',
117
+ // Field names, so hand-translated; prisma-compat narrows write results itself.
118
+ select: 'prisma',
119
+ omit: 'prisma',
117
120
  timeout: 'native',
118
121
  };
119
122
  export const CREATE_MANY_OPTIONS = {
@@ -124,6 +127,9 @@ export const CREATE_MANY_OPTIONS = {
124
127
  export const UPDATE_OPTIONS = {
125
128
  where: 'prisma',
126
129
  data: 'prisma',
130
+ // Field names, so hand-translated; prisma-compat narrows write results itself.
131
+ select: 'prisma',
132
+ omit: 'prisma',
127
133
  // `{ field, expected }`, and `field` is a FIELD NAME, so it has to be renamed
128
134
  // into turbine's naming space rather than copied. See THE ONE RULE above.
129
135
  optimisticLock: 'prisma',
@@ -140,6 +146,9 @@ export const UPDATE_MANY_OPTIONS = {
140
146
  };
141
147
  export const DELETE_OPTIONS = {
142
148
  where: 'prisma',
149
+ // Field names, so hand-translated; prisma-compat narrows write results itself.
150
+ select: 'prisma',
151
+ omit: 'prisma',
143
152
  timeout: 'native',
144
153
  allowFullTableScan: 'native',
145
154
  skipGlobalFilters: 'native',
@@ -152,6 +161,9 @@ export const DELETE_MANY_OPTIONS = {
152
161
  };
153
162
  export const UPSERT_OPTIONS = {
154
163
  where: 'prisma',
164
+ // Field names, so hand-translated; prisma-compat narrows write results itself.
165
+ select: 'prisma',
166
+ omit: 'prisma',
155
167
  create: 'prisma',
156
168
  update: 'prisma',
157
169
  timeout: 'native',
@@ -870,12 +870,28 @@ export interface FindManyStreamArgs<T, R extends object = {}, W extends TypedWit
870
870
  */
871
871
  batchSize?: number;
872
872
  }
873
- export interface CreateArgs<T, R extends object = {}> {
873
+ export interface CreateArgs<T, R extends object = {}, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined> {
874
874
  /**
875
875
  * Row data. On typed clients, relation names additionally accept nested
876
876
  * write ops ({@link NestedCreateOp}): `create` / `connect` / `connectOrCreate`.
877
877
  */
878
878
  data: CreateDataInput<T, R>;
879
+ /**
880
+ * Return only these fields of the written row, instead of the whole row.
881
+ * Keys are checked against `T` (see {@link FieldFlags}) and follow the read
882
+ * rules: an unknown name throws E003, a `select` must name at least one field,
883
+ * and it cannot be combined with `omit`. Narrows the `RETURNING` list itself,
884
+ * so the unselected columns never cross the wire, which is the point on a
885
+ * table with a large JSON or text column. Naming a PII-tagged column here is
886
+ * the explicit opt-in that returns it, exactly as on a read.
887
+ */
888
+ select?: S & FieldFlags<T, S>;
889
+ /**
890
+ * Return the written row without these fields. Same rules as `select`;
891
+ * PII-tagged columns stay excluded, as they are from every default write
892
+ * return.
893
+ */
894
+ omit?: O & FieldFlags<T, O>;
879
895
  /** Query timeout in milliseconds. Rejects with an error if exceeded. */
880
896
  timeout?: number;
881
897
  }
@@ -916,7 +932,7 @@ export type UpdateOperatorInput<V> = {
916
932
  export type UpdateInput<T> = {
917
933
  [K in keyof T]?: T[K] | UpdateOperatorInput<T[K]>;
918
934
  };
919
- export interface UpdateArgs<T, R extends object = {}> {
935
+ export interface UpdateArgs<T, R extends object = {}, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined> {
920
936
  /** Row selector. Keys are checked against `T` and `R` (see {@link WhereClause}). */
921
937
  where: WhereClause<T, R>;
922
938
  /**
@@ -925,6 +941,22 @@ export interface UpdateArgs<T, R extends object = {}> {
925
941
  * / `disconnect` / `set` / `delete` / `update` / `upsert`.
926
942
  */
927
943
  data: UpdateDataInput<T, R>;
944
+ /**
945
+ * Return only these fields of the updated row, instead of the whole row.
946
+ * Keys are checked against `T` (see {@link FieldFlags}) and follow the read
947
+ * rules: an unknown name throws E003, a `select` must name at least one field,
948
+ * and it cannot be combined with `omit`. Narrows the `RETURNING` list itself,
949
+ * so the unselected columns never cross the wire, which is the point on a
950
+ * table with a large JSON or text column. Naming a PII-tagged column here is
951
+ * the explicit opt-in that returns it, exactly as on a read.
952
+ */
953
+ select?: S & FieldFlags<T, S>;
954
+ /**
955
+ * Return the updated row without these fields. Same rules as `select`;
956
+ * PII-tagged columns stay excluded, as they are from every default write
957
+ * return.
958
+ */
959
+ omit?: O & FieldFlags<T, O>;
928
960
  /** Query timeout in milliseconds. Rejects with an error if exceeded. */
929
961
  timeout?: number;
930
962
  /**
@@ -974,8 +1006,24 @@ export interface UpdateManyArgs<T, R extends object = {}> {
974
1006
  /** Opt out of configured {@link GlobalFilters}. See {@link SkipGlobalFilters}. */
975
1007
  skipGlobalFilters?: SkipGlobalFilters;
976
1008
  }
977
- export interface DeleteArgs<T, R extends object = {}> {
1009
+ export interface DeleteArgs<T, R extends object = {}, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined> {
978
1010
  where: WhereClause<T, R>;
1011
+ /**
1012
+ * Return only these fields of the deleted row, instead of the whole row.
1013
+ * Keys are checked against `T` (see {@link FieldFlags}) and follow the read
1014
+ * rules: an unknown name throws E003, a `select` must name at least one field,
1015
+ * and it cannot be combined with `omit`. Narrows the `RETURNING` list itself,
1016
+ * so the unselected columns never cross the wire, which is the point on a
1017
+ * table with a large JSON or text column. Naming a PII-tagged column here is
1018
+ * the explicit opt-in that returns it, exactly as on a read.
1019
+ */
1020
+ select?: S & FieldFlags<T, S>;
1021
+ /**
1022
+ * Return the deleted row without these fields. Same rules as `select`;
1023
+ * PII-tagged columns stay excluded, as they are from every default write
1024
+ * return.
1025
+ */
1026
+ omit?: O & FieldFlags<T, O>;
979
1027
  /** Query timeout in milliseconds. Rejects with an error if exceeded. */
980
1028
  timeout?: number;
981
1029
  /** See {@link UpdateArgs.allowFullTableScan}. */
@@ -992,8 +1040,24 @@ export interface DeleteManyArgs<T, R extends object = {}> {
992
1040
  /** Opt out of configured {@link GlobalFilters}. See {@link SkipGlobalFilters}. */
993
1041
  skipGlobalFilters?: SkipGlobalFilters;
994
1042
  }
995
- export interface UpsertArgs<T, R extends object = {}> {
1043
+ export interface UpsertArgs<T, R extends object = {}, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined> {
996
1044
  where: WhereClause<T, R>;
1045
+ /**
1046
+ * Return only these fields of the inserted or updated row, instead of the whole row.
1047
+ * Keys are checked against `T` (see {@link FieldFlags}) and follow the read
1048
+ * rules: an unknown name throws E003, a `select` must name at least one field,
1049
+ * and it cannot be combined with `omit`. Narrows the `RETURNING` list itself,
1050
+ * so the unselected columns never cross the wire, which is the point on a
1051
+ * table with a large JSON or text column. Naming a PII-tagged column here is
1052
+ * the explicit opt-in that returns it, exactly as on a read.
1053
+ */
1054
+ select?: S & FieldFlags<T, S>;
1055
+ /**
1056
+ * Return the inserted or updated row without these fields. Same rules as `select`;
1057
+ * PII-tagged columns stay excluded, as they are from every default write
1058
+ * return.
1059
+ */
1060
+ omit?: O & FieldFlags<T, O>;
997
1061
  /** The row to insert when `where` matches nothing. Plain values only: there is no stored value to operate on yet. */
998
1062
  create: Partial<T>;
999
1063
  /**