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
package/dist/powql.js CHANGED
@@ -35,9 +35,10 @@
35
35
  * @module
36
36
  */
37
37
  import { randomUUID } from 'node:crypto';
38
+ import { guardCheckout } from './connection-guard.js';
38
39
  import { NotFoundError, OptimisticLockError, ReadOnlyError, TimeoutError, UnsupportedFeatureError, ValidationError, } from './errors.js';
39
40
  import { executeNestedCreate, executeNestedUpdate, hasRelationFields, } from './nested-write.js';
40
- import { ALL_POWDB_CAPABILITIES, baseTsType, coerceNativeValue, isJsonColumn, isPowdbDatetimeColumn, isStaleFramePowdbError, PowdbFloatParam, PowdbJsonParam, powqlColumnType, quotePowqlDotted, quotePowqlIdent, requireCapability, rowToEntity, } from './powdb-shared.js';
41
+ import { ALL_POWDB_CAPABILITIES, atLeastVersion, baseTsType, coerceNativeValue, isJsonColumn, isPowdbDatetimeColumn, isStaleFramePowdbError, PowdbFloatParam, PowdbJsonParam, parsePowdbSemver, powqlColumnType, quotePowqlDotted, quotePowqlIdent, requireCapability, rowToEntity, } from './powdb-shared.js';
41
42
  import { assertAggregatePiiOptIn } from './query/aggregates.js';
42
43
  import { assertMutationWhereIdentifiesOneRow, assertWhereIdentifiesOneRow, expandCompoundUniqueWhere, } from './query/compound-unique.js';
43
44
  import { ARRAY_OPERATOR_KEYS, isJsonFilter, isRelationPickOrderBy, orderByEntries } from './query/filters.js';
@@ -1236,6 +1237,34 @@ export class PowqlInterface {
1236
1237
  }
1237
1238
  return entity;
1238
1239
  }
1240
+ /**
1241
+ * A single-row write's `select` / `omit`, resolved by {@link projectionPlan}
1242
+ * so the rules and messages are the read path's, and the SQL engines' (an
1243
+ * unknown name is E003, `select` must name something, the pair is refused).
1244
+ * `undefined` for the default return shape. Resolved BEFORE the write is sent,
1245
+ * so a bad projection writes nothing.
1246
+ *
1247
+ * The narrowing itself happens on the returned row ({@link shapeWriteRow}):
1248
+ * PowQL's `returning` takes no column list (see {@link stripWritePii}), so
1249
+ * unlike the SQL engines the unselected columns still cross the wire here.
1250
+ * The RESULT is identical across engines; the byte saving is SQL-only.
1251
+ */
1252
+ writeReturnPlan(args) {
1253
+ if (args.select === undefined && args.omit === undefined)
1254
+ return undefined;
1255
+ return this.projectionPlan(args.select, args.omit, false);
1256
+ }
1257
+ /** Apply a write's return plan to its row, or the default PII strip without one. */
1258
+ shapeWriteRow(entity, plan) {
1259
+ if (!entity || !plan)
1260
+ return this.stripWritePii(entity);
1261
+ const keep = new Set(plan.cols.filter((c) => !plan.forcedPk.includes(c)).map((c) => this.meta.reverseColumnMap[c] ?? c));
1262
+ for (const key of Object.keys(entity)) {
1263
+ if (!keep.has(key))
1264
+ delete entity[key];
1265
+ }
1266
+ return entity;
1267
+ }
1239
1268
  /** `{ .c1, .c2, … }` projection clause. */
1240
1269
  projection(cols) {
1241
1270
  return `{ ${cols.map((c) => this.colRefName(c)).join(', ')} }`;
@@ -2676,6 +2705,7 @@ export class PowqlInterface {
2676
2705
  }
2677
2706
  async create(args) {
2678
2707
  return this.withMiddleware('create', args, async () => {
2708
+ const plan = this.writeReturnPlan(args);
2679
2709
  if (hasRelationFields(args.data, this.meta)) {
2680
2710
  return this.nestedCreate(args);
2681
2711
  }
@@ -2687,7 +2717,7 @@ export class PowqlInterface {
2687
2717
  .join(', ');
2688
2718
  // `returning` surfaces the inserted row (all columns, schema order) in one round-trip.
2689
2719
  const { rows, native } = await this.exec(`insert ${this.qt} { ${body} } returning`, params, args.timeout, 'create');
2690
- const row = rows.length ? this.stripWritePii(this.shape(rows, native)[0]) : null;
2720
+ const row = rows.length ? this.shapeWriteRow(this.shape(rows, native)[0], plan) : null;
2691
2721
  if (!row)
2692
2722
  throw new NotFoundError({ table: this.table, where: data });
2693
2723
  return row;
@@ -2762,6 +2792,7 @@ export class PowqlInterface {
2762
2792
  }
2763
2793
  async update(args) {
2764
2794
  return this.withMiddleware('update', args, async () => {
2795
+ const plan = this.writeReturnPlan(args);
2765
2796
  if (hasRelationFields(args.data, this.meta)) {
2766
2797
  // The nested engine re-enters `update` / `findUnique` for the parent
2767
2798
  // row before it writes anything, so the identity rule below still runs
@@ -2799,7 +2830,7 @@ export class PowqlInterface {
2799
2830
  }
2800
2831
  // `returning` hands back the post-update row(s); take the first (single-row contract).
2801
2832
  const { rows, native } = await this.exec(`${this.qt} filter ${where} update { ${setClause} } returning`, params, args.timeout, 'update');
2802
- const row = rows.length ? this.stripWritePii(this.shape(rows, native)[0]) : null;
2833
+ const row = rows.length ? this.shapeWriteRow(this.shape(rows, native)[0], plan) : null;
2803
2834
  if (!row) {
2804
2835
  if (lock) {
2805
2836
  throw new OptimisticLockError({
@@ -2876,18 +2907,21 @@ export class PowqlInterface {
2876
2907
  }
2877
2908
  async nestedCreate(args) {
2878
2909
  const data = args.data;
2910
+ // Applied by the nested engine's final read-back, as on the SQL engines.
2911
+ const shape = { select: args.select, omit: args.omit };
2879
2912
  if (this.isTxScoped()) {
2880
- return executeNestedCreate(this.buildNestedCtx(), this.table, data);
2913
+ return executeNestedCreate(this.buildNestedCtx(), this.table, data, 0, [], shape);
2881
2914
  }
2882
- return this.runInImplicitTx((ctx) => executeNestedCreate(ctx, this.table, data));
2915
+ return this.runInImplicitTx((ctx) => executeNestedCreate(ctx, this.table, data, 0, [], shape));
2883
2916
  }
2884
2917
  async nestedUpdate(args) {
2885
2918
  const data = args.data;
2886
2919
  const where = args.where;
2920
+ const shape = { select: args.select, omit: args.omit };
2887
2921
  if (this.isTxScoped()) {
2888
- return executeNestedUpdate(this.buildNestedCtx(), this.table, where, data);
2922
+ return executeNestedUpdate(this.buildNestedCtx(), this.table, where, data, 0, [], shape);
2889
2923
  }
2890
- return this.runInImplicitTx((ctx) => executeNestedUpdate(ctx, this.table, where, data));
2924
+ return this.runInImplicitTx((ctx) => executeNestedUpdate(ctx, this.table, where, data, 0, [], shape));
2891
2925
  }
2892
2926
  /** Open a flat PowDB transaction on a pinned connection and run `fn` inside it. */
2893
2927
  async runInImplicitTx(fn) {
@@ -2900,6 +2934,9 @@ export class PowqlInterface {
2900
2934
  // drifts from `powdbDialect`; falls back to the literal lowercase keywords.
2901
2935
  const d = this.options.dialect;
2902
2936
  const client = await this.pool.connect();
2937
+ // A no-op today (PowDB checkouts have no event surface), kept so this path
2938
+ // cannot become the one unguarded checkout if a transport ever emits 'error'.
2939
+ const checkout = guardCheckout(client);
2903
2940
  let began = false;
2904
2941
  try {
2905
2942
  await client.query(d?.beginStatement?.() ?? 'begin');
@@ -2939,7 +2976,7 @@ export class PowqlInterface {
2939
2976
  throw err;
2940
2977
  }
2941
2978
  finally {
2942
- client.release();
2979
+ checkout.release();
2943
2980
  }
2944
2981
  }
2945
2982
  /** Already inside a transaction: build a context whose table accessors reuse the pinned pool. */
@@ -2952,6 +2989,7 @@ export class PowqlInterface {
2952
2989
  }
2953
2990
  async delete(args) {
2954
2991
  return this.withMiddleware('delete', args, async () => {
2992
+ const plan = this.writeReturnPlan(args);
2955
2993
  const allowFullTableScan = resolveUnsafeFlag(args.allowFullTableScan, 'allowFullTableScan');
2956
2994
  const userWhere = this.expandedWhere(args.where);
2957
2995
  const params = [];
@@ -2962,7 +3000,7 @@ export class PowqlInterface {
2962
3000
  where = this.applyGlobalFilter(where, params, args.skipGlobalFilters);
2963
3001
  // `returning` hands back the deleted row(s), no separate pre-image reselect needed.
2964
3002
  const { rows, native } = await this.exec(`${this.qt} filter ${where} delete returning`, params, args.timeout, 'delete');
2965
- const row = rows.length ? this.stripWritePii(this.shape(rows, native)[0]) : null;
3003
+ const row = rows.length ? this.shapeWriteRow(this.shape(rows, native)[0], plan) : null;
2966
3004
  if (!row)
2967
3005
  throw new NotFoundError({ table: this.table, where: args.where });
2968
3006
  return row;
@@ -2988,13 +3026,45 @@ export class PowqlInterface {
2988
3026
  // disagreeing about whether a query is VALID, which is the failure class
2989
3027
  // 0.64.0 and 0.72.0 were both spent on. Before `applyPkDefault`, so a
2990
3028
  // refused upsert mints no client-side UUID and sends nothing.
2991
- this.assertMutationIdentifiesOneRow(this.expandedWhere(args.where), false, 'upsert');
3029
+ const upsertWhere = (this.expandedWhere(args.where) ?? {});
3030
+ this.assertMutationIdentifiesOneRow(upsertWhere, false, 'upsert');
3031
+ const plan = this.writeReturnPlan(args);
2992
3032
  const createData = this.applyPkDefault(args.create);
2993
3033
  const pkCol = this.meta.primaryKey[0];
2994
- if (this.meta.primaryKey.length !== 1 || !pkCol) {
2995
- // PowQL's native `upsert … on .col` takes a single conflict column, so a
2996
- // composite PK falls back to an atomic reselect-or-write transaction.
2997
- return this.upsertComposite(createData, args.update);
3034
+ // The conflict target is the columns `where` names, as on the SQL
3035
+ // engines (`ON CONFLICT (<where keys>)`), with the conflicting VALUES
3036
+ // taken from `create`. This path used to conflict on the primary key
3037
+ // whatever `where` named, so an upsert keyed on another unique column
3038
+ // (`where: { email }`) never found the existing row.
3039
+ const conflictColumns = Object.keys(upsertWhere)
3040
+ .filter((k) => upsertWhere[k] !== undefined)
3041
+ .map((k) => this.column(k).name);
3042
+ // PowQL's native `upsert … on .col` expresses exactly one shape: a single
3043
+ // conflict column that is the primary key, with no predicate on its
3044
+ // conflict branch. Everything else is an atomic reselect-or-write
3045
+ // transaction.
3046
+ //
3047
+ // That includes a table under a global filter. The native statement's
3048
+ // `on conflict` branch carries no predicate, so a tenant-scoped client
3049
+ // whose `create` key matched ANOTHER tenant's row updated that row, and
3050
+ // the tenant-filtered reselect then reported the write it had just made
3051
+ // as a NotFoundError. The SQL engines guard the same branch with the
3052
+ // filter (`DO UPDATE … WHERE tenant_id = $n`). The lookup-first path runs
3053
+ // its find and its update through the filtered interface, so another
3054
+ // tenant's row is invisible to it: the insert then collides on the key
3055
+ // and fails, as it does on the SQL engines, and nothing is overwritten.
3056
+ const filtered = this.applyGlobalFilter('', [], args.skipGlobalFilters) !== '';
3057
+ const native = this.meta.primaryKey.length === 1 &&
3058
+ pkCol !== undefined &&
3059
+ conflictColumns.length === 1 &&
3060
+ conflictColumns[0] === pkCol &&
3061
+ !filtered;
3062
+ if (!native) {
3063
+ return this.upsertLookupFirst(createData, args.update, conflictColumns, {
3064
+ select: args.select,
3065
+ omit: args.omit,
3066
+ skipGlobalFilters: args.skipGlobalFilters,
3067
+ });
2998
3068
  }
2999
3069
  const params = [];
3000
3070
  const createBody = this.scalarData(createData)
@@ -3007,40 +3077,61 @@ export class PowqlInterface {
3007
3077
  // reselect-by-PK fetch; create/update/delete all use `returning`.
3008
3078
  await this.exec(`upsert ${this.qt} on ${this.colRefName(pkCol)} { ${createBody} } on conflict { ${updateBody} }`, params, args.timeout, 'upsert');
3009
3079
  const pkField = this.meta.reverseColumnMap[pkCol] ?? pkCol;
3010
- // Either spelling, as `upsertComposite` already does below: reading the
3080
+ // Either spelling, as `upsertLookupFirst` already does below: reading the
3011
3081
  // camelCase field alone reselected `undefined` for a snake-spelled PK and
3012
3082
  // reported a write that had SUCCEEDED as a NotFoundError.
3013
- const row = await this.reselectByPk(createData[pkField] ?? createData[pkCol], args.timeout);
3083
+ // `args` carries skipGlobalFilters too: an upsert that opted out of the
3084
+ // filter must not have its own write filtered out of the reselect.
3085
+ const row = await this.reselectByPk(createData[pkField] ?? createData[pkCol], args.timeout, args);
3014
3086
  if (!row)
3015
3087
  throw new NotFoundError({ table: this.table, where: createData });
3016
- return row;
3088
+ // Without a plan the reselect's default projection already left PII out.
3089
+ return plan ? this.shapeWriteRow(row, plan) : row;
3017
3090
  });
3018
3091
  }
3019
3092
  /**
3020
- * Composite-key upsert: PowQL's `upsert … on .col` only takes one conflict
3021
- * column, so reselect by the full composite PK and update-or-create inside one
3022
- * flat transaction (PowDB single-writer makes the read-then-write safe from
3023
- * concurrent writers; the transaction makes it atomic with the write).
3024
- */
3025
- async upsertComposite(createData, updateData) {
3093
+ * Upsert as a reselect-or-write inside one flat transaction, for every shape
3094
+ * the native `upsert … on .col` statement cannot express: a conflict target
3095
+ * other than a single-column primary key (it takes one column and PowDB has
3096
+ * no composite unique), and a table under a global filter (its conflict
3097
+ * branch takes no predicate). PowDB's single writer makes the read-then-write
3098
+ * safe from concurrent writers; the transaction makes it atomic with the
3099
+ * write.
3100
+ *
3101
+ * The row is looked up by the conflict columns with `create`'s values for
3102
+ * them, which is what `ON CONFLICT (<cols>)` compares on the SQL engines. The
3103
+ * find and the update run through the transaction's table interface, so a
3104
+ * configured global filter applies to both exactly as it does to any other
3105
+ * read or write, and `skipGlobalFilters` is forwarded to both.
3106
+ */
3107
+ async upsertLookupFirst(createData, updateData, conflictColumns, shape) {
3108
+ const { skipGlobalFilters, ...projection } = shape;
3109
+ const skip = skipGlobalFilters === undefined ? {} : { skipGlobalFilters };
3026
3110
  // Accept either the camelCase field or the snake_case column in `create`
3027
3111
  // (create() resolves both), and key the where by field name.
3028
- const pkPairs = this.meta.primaryKey.map((pk) => {
3029
- const field = this.meta.reverseColumnMap[pk] ?? pk;
3030
- return { field, value: createData[field] ?? createData[pk] };
3112
+ const keyPairs = conflictColumns.map((col) => {
3113
+ const field = this.meta.reverseColumnMap[col] ?? col;
3114
+ return { field, value: createData[field] ?? createData[col] };
3031
3115
  });
3032
- if (pkPairs.some((p) => p.value == null)) {
3033
- throw new ValidationError(`upsert on "${this.table}" needs every composite-PK field in \`create\` (${pkPairs
3116
+ const keyless = keyPairs.some((p) => p.value == null);
3117
+ const isCompositePk = keyPairs.length > 1 &&
3118
+ keyPairs.length === this.meta.primaryKey.length &&
3119
+ conflictColumns.every((c) => this.meta.primaryKey.includes(c));
3120
+ if (keyless && isCompositePk) {
3121
+ throw new ValidationError(`upsert on "${this.table}" needs every composite-PK field in \`create\` (${keyPairs
3034
3122
  .map((p) => p.field)
3035
3123
  .join(', ')}).`);
3036
3124
  }
3037
- const pkWhere = Object.fromEntries(pkPairs.map((p) => [p.field, p.value]));
3125
+ const keyWhere = Object.fromEntries(keyPairs.map((p) => [p.field, p.value]));
3038
3126
  const run = async (ctx) => {
3039
3127
  const tbl = ctx.tx.table(this.table);
3040
- const existing = await tbl.findUnique({ where: pkWhere });
3128
+ // A row whose `create` leaves a conflict column unset cannot collide on
3129
+ // it (NULL never equals NULL in a unique index, and a server-assigned key
3130
+ // is new), which is what `ON CONFLICT` concludes on the SQL engines too.
3131
+ const existing = keyless ? null : await tbl.findUnique({ where: keyWhere, ...skip });
3041
3132
  return existing
3042
- ? (await tbl.update({ where: pkWhere, data: updateData }))
3043
- : (await tbl.create({ data: createData }));
3133
+ ? (await tbl.update({ where: keyWhere, data: updateData, ...projection, ...skip }))
3134
+ : (await tbl.create({ data: createData, ...projection }));
3044
3135
  };
3045
3136
  return this.isTxScoped() ? run(this.buildNestedCtx()) : this.runInImplicitTx(run);
3046
3137
  }
@@ -3087,33 +3178,64 @@ export class PowqlInterface {
3087
3178
  'every SQL engine exactly when the column is nullable. `_count: true` (a row count) and a per-field ' +
3088
3179
  '`_count` of a NOT NULL column are correct on every version and are never refused.');
3089
3180
  }
3181
+ /**
3182
+ * Can `aggregate()` compute several aggregates in ONE statement on this engine?
3183
+ *
3184
+ * PowQL refuses a bare multi-aggregate projection (`T { a: sum(.x), b: sum(.y) }`
3185
+ * is "aggregate function in an unsupported position" on every engine version),
3186
+ * so the only one-statement form is a grouping over a literal key,
3187
+ * `T filter … group 1 { agg_0: sum(.x), agg_1: sum(.y) }`, which yields a
3188
+ * single group holding every row the filter matched. Measured against the
3189
+ * embedded addon at 0.7.1 through 0.28: below 0.13 a literal group key does not
3190
+ * parse, from 0.13 to 0.19.1 the grouped per-field `count` disagrees with the
3191
+ * scalar `count(T { .col })` on a nullable column, and from 0.20 every
3192
+ * aggregate kind (count / sum / avg / min / max over int, float, str, an
3193
+ * all-null column, and several filters) answers identically on both wires.
3194
+ *
3195
+ * Derived from the PROBED engine version rather than from a capability flag,
3196
+ * so an unprobed pool (`engineVersion: null`, e.g. an injected pool carrying
3197
+ * {@link ALL_POWDB_CAPABILITIES}) keeps the per-field statements: this changes
3198
+ * the emitted PowQL, and an older engine would reject it outright, the same
3199
+ * probe-only discipline `nestedProjections` follows.
3200
+ */
3201
+ get groupsAggregatesInOneStatement() {
3202
+ const sem = parsePowdbSemver(this.capabilities.engineVersion);
3203
+ return sem !== null && atLeastVersion(sem, 0, 20);
3204
+ }
3090
3205
  async aggregate(args) {
3091
3206
  this.assertNoForceCustomPlan(args);
3092
3207
  return this.withMiddleware('aggregate', args, async () => {
3093
- // One scalar query per aggregate, PowDB's bare-projection aggregate is broken.
3094
3208
  const result = {};
3095
3209
  const filterParams = [];
3096
3210
  const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
3097
3211
  let where = this.buildWhere(resolvedWhere, filterParams);
3098
3212
  where = this.applyGlobalFilter(where, filterParams, args.skipGlobalFilters);
3099
3213
  const filter = where ? ` filter ${where}` : '';
3100
- const scalar = async (expr) => {
3101
- const params = [...filterParams];
3102
- const { rows } = await this.exec(expr, params, args.timeout, 'aggregate');
3103
- const v = rows[0]?.value;
3104
- return v == null || v === 'null' ? null : Number(v);
3105
- };
3214
+ const plan = [];
3106
3215
  if (args._count) {
3107
3216
  if (args._count === true) {
3108
- result._count = (await scalar(`count(${this.qt}${filter})`)) ?? 0;
3217
+ plan.push({
3218
+ scalar: `count(${this.qt}${filter})`,
3219
+ inner: 'count(*)',
3220
+ assign: (v) => {
3221
+ result._count = v ?? 0;
3222
+ },
3223
+ });
3109
3224
  }
3110
3225
  else {
3111
3226
  const counts = {};
3227
+ result._count = counts;
3112
3228
  for (const field of Object.keys(args._count).filter((f) => args._count[f])) {
3113
3229
  this.assertProjectedCountSupported(field);
3114
- counts[field] = (await scalar(`count(${this.qt}${filter} { ${this.ref(field)} })`)) ?? 0;
3230
+ const ref = this.ref(field);
3231
+ plan.push({
3232
+ scalar: `count(${this.qt}${filter} { ${ref} })`,
3233
+ inner: `count(${ref})`,
3234
+ assign: (v) => {
3235
+ counts[field] = v ?? 0;
3236
+ },
3237
+ });
3115
3238
  }
3116
- result._count = counts;
3117
3239
  }
3118
3240
  }
3119
3241
  for (const fn of ['_sum', '_avg', '_min', '_max']) {
@@ -3121,15 +3243,44 @@ export class PowqlInterface {
3121
3243
  if (!spec)
3122
3244
  continue;
3123
3245
  const acc = {};
3246
+ result[fn] = acc;
3247
+ const powfn = fn.slice(1); // sum/avg/min/max
3124
3248
  for (const field of Object.keys(spec).filter((f) => spec[f])) {
3125
- const powfn = fn.slice(1); // sum/avg/min/max
3126
3249
  // Same PII contract as the SQL engines: _min/_max return a stored cell.
3127
3250
  if (fn === '_min' || fn === '_max') {
3128
3251
  assertAggregatePiiOptIn(this.table, this.meta, field, this.column(field).name, `aggregate ${fn}`, resolveUnsafeFlag(args.includePii, 'includePii'));
3129
3252
  }
3130
- acc[field] = await scalar(`${powfn}(${this.qt}${filter} { ${this.ref(field)} })`);
3253
+ const ref = this.ref(field);
3254
+ plan.push({
3255
+ scalar: `${powfn}(${this.qt}${filter} { ${ref} })`,
3256
+ inner: `${powfn}(${ref})`,
3257
+ assign: (v) => {
3258
+ acc[field] = v;
3259
+ },
3260
+ });
3131
3261
  }
3132
- result[fn] = acc;
3262
+ }
3263
+ // A scalar cell and a grouped cell decode through the same wire decoder,
3264
+ // so one coercion serves both forms.
3265
+ const toNumber = (v) => (v == null || v === 'null' ? null : Number(v));
3266
+ if (plan.length > 1 && this.groupsAggregatesInOneStatement) {
3267
+ const proj = plan.map((p, i) => `agg_${i}: ${p.inner}`).join(', ');
3268
+ const { rows } = await this.exec(`${this.qt}${filter} group 1 { ${proj} }`, [...filterParams], args.timeout, 'aggregate');
3269
+ // Zero groups means the filter matched no row. The per-field statements
3270
+ // below answer that case instead of a synthesized one, because what an
3271
+ // aggregate over NO rows returns is engine-versioned (`sum` answered 0
3272
+ // before PowDB 0.28 and null since), and the scalar form is the one
3273
+ // whose answer this method has always returned.
3274
+ const row = rows.length === 1 ? rows[0] : undefined;
3275
+ if (row) {
3276
+ for (const [i, p] of plan.entries())
3277
+ p.assign(toNumber(row[`agg_${i}`]));
3278
+ return result;
3279
+ }
3280
+ }
3281
+ for (const p of plan) {
3282
+ const { rows } = await this.exec(p.scalar, [...filterParams], args.timeout, 'aggregate');
3283
+ p.assign(toNumber(rows[0]?.value));
3133
3284
  }
3134
3285
  return result;
3135
3286
  });
@@ -3532,12 +3683,15 @@ export class PowqlInterface {
3532
3683
  // Reselect helper (upsert only, PowDB's upsert has no `returning`)
3533
3684
  // -------------------------------------------------------------------------
3534
3685
  /** Reselect a single row by its single-column primary key value. */
3535
- async reselectByPk(pkValue, timeout) {
3686
+ async reselectByPk(pkValue, timeout, shape) {
3536
3687
  const pkField = this.meta.reverseColumnMap[this.meta.primaryKey[0]] ?? this.meta.primaryKey[0];
3537
3688
  const { rows, native } = await this.runFind({
3538
3689
  where: { [pkField]: pkValue },
3539
3690
  limit: 1,
3540
3691
  timeout,
3692
+ select: shape?.select,
3693
+ omit: shape?.omit,
3694
+ ...(shape?.skipGlobalFilters === undefined ? {} : { skipGlobalFilters: shape.skipGlobalFilters }),
3541
3695
  });
3542
3696
  return rows.length ? this.shape(rows, native)[0] : null;
3543
3697
  }
@@ -968,6 +968,22 @@ function resolveWriteProjection(ctx, mm, op, args) {
968
968
  }
969
969
  return NO_WRITE_PROJECTION;
970
970
  }
971
+ /**
972
+ * The same projection in core's spelling, for the write itself. Passed DOWN so
973
+ * the SQL engines narrow the RETURNING list rather than fetching the whole row
974
+ * for {@link applyWriteProjection} to trim afterwards, which on a table with a
975
+ * large JSON column was most of the bytes the write moved. The trim stays: it
976
+ * is what keeps the result Prisma-shaped on every path, including the
977
+ * lookup-first upsert and PowDB, whose `returning` takes no column list.
978
+ */
979
+ function coreWriteProjection(mm, proj) {
980
+ const spell = (keys) => Object.fromEntries([...keys].map((k) => [renameField(mm, k), true]));
981
+ if (proj.pick)
982
+ return { select: spell(proj.pick) };
983
+ if (proj.omit)
984
+ return { omit: spell(proj.omit) };
985
+ return {};
986
+ }
971
987
  /** Apply a {@link WriteProjection} to one already-reshaped write result row. */
972
988
  function applyWriteProjection(proj, row) {
973
989
  if (!isPlainObject(row))
@@ -1762,9 +1778,12 @@ function upsertKeysMatch(t) {
1762
1778
  /** Prisma upsert semantics: look up by where; update the found row, else insert create. */
1763
1779
  async function upsertLookupFirst(qi, t) {
1764
1780
  const existing = await qi.findUnique({ where: t.where });
1781
+ // The projection rides along so both branches return what the native upsert
1782
+ // path returns (an explicitly selected PII field included).
1783
+ const shape = { select: t.select, omit: t.omit };
1765
1784
  if (existing)
1766
- return qi.update({ where: t.where, data: t.update });
1767
- return qi.create({ data: t.create });
1785
+ return qi.update({ where: t.where, data: t.update, ...shape });
1786
+ return qi.create({ data: t.create, ...shape });
1768
1787
  }
1769
1788
  /**
1770
1789
  * The row shapes a translated `createMany` has to insert, as contiguous runs
@@ -1872,7 +1891,10 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
1872
1891
  const shape = (raw) => applyWriteProjection(proj, reshapeRow(ctx, mm, raw));
1873
1892
  return defer('create', args, () => {
1874
1893
  proj = resolveWriteProjection(ctx, mm, 'create', args);
1875
- const t = { data: translateWriteData(ctx, mm, applyCreateDefaults(mm, args.data)) };
1894
+ const t = {
1895
+ data: translateWriteData(ctx, mm, applyCreateDefaults(mm, args.data)),
1896
+ ...coreWriteProjection(mm, proj),
1897
+ };
1876
1898
  applyNativeOptions(CREATE_OPTIONS, args, t);
1877
1899
  return t;
1878
1900
  }, (qi, t) => qi.create(t).then(shape), {
@@ -1917,6 +1939,7 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
1917
1939
  const t = {
1918
1940
  where: translateWhere(ctx, mm, a.where),
1919
1941
  data: translateWriteData(ctx, mm, applyUpdateTouch(mm, a.data)),
1942
+ ...coreWriteProjection(mm, proj),
1920
1943
  };
1921
1944
  applyNativeOptions(UPDATE_OPTIONS, a, t);
1922
1945
  // `optimisticLock.field` is a FIELD NAME, so it is renamed rather than
@@ -1956,7 +1979,7 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
1956
1979
  return defer('delete', args, () => {
1957
1980
  const a = requireWhere(args, 'delete');
1958
1981
  proj = resolveWriteProjection(ctx, mm, 'delete', a);
1959
- const t = { where: translateWhere(ctx, mm, a.where) };
1982
+ const t = { where: translateWhere(ctx, mm, a.where), ...coreWriteProjection(mm, proj) };
1960
1983
  applyNativeOptions(DELETE_OPTIONS, a, t);
1961
1984
  return t;
1962
1985
  }, (qi, t) => qi.delete(t).then(shape), { build: (qi, t) => qi.buildDelete(t), reshape: shape });
@@ -1981,6 +2004,7 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
1981
2004
  where: translateWhere(ctx, mm, a.where),
1982
2005
  create: translateWriteData(ctx, mm, applyCreateDefaults(mm, a.create)),
1983
2006
  update: translateWriteData(ctx, mm, applyUpdateTouch(mm, a.update)),
2007
+ ...coreWriteProjection(mm, proj),
1984
2008
  };
1985
2009
  applyNativeOptions(UPSERT_OPTIONS, a, t);
1986
2010
  return t;
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import type { PgCompatPool } from '../pg-types.js';
14
14
  import type { SchemaMetadata } from '../schema.js';
15
- import type { AggregateArgs, AggregateResult, CountArgs, CreateArgs, CreateManyArgs, DeleteArgs, DeleteManyArgs, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GroupByArgs, GroupByResult, QueryResult, TypedWithClause, UpdateArgs, UpdateManyArgs, UpsertArgs, WithClause } from './types.js';
15
+ import type { AggregateArgs, AggregateResult, CountArgs, CreateArgs, CreateManyArgs, DeleteArgs, DeleteManyArgs, FieldResult, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GroupByArgs, GroupByResult, QueryResult, TypedWithClause, UpdateArgs, UpdateManyArgs, UpsertArgs, WithClause } from './types.js';
16
16
  /**
17
17
  * Discard the memoized cross-check environment so the next cache hit re-reads
18
18
  * `process.env`.
@@ -993,10 +993,34 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
993
993
  * here and is not refused.
994
994
  */
995
995
  private preparedNameFor;
996
+ /**
997
+ * {@link queryWithTimeout} for a statement that only READS, which makes it
998
+ * safe to send twice. Outside a transaction, a read that fails because the
999
+ * connection it went out on had already been closed by the server
1000
+ * ({@link isStaleConnectionError}) is sent once more, after one event-loop
1001
+ * turn, on whatever connection the pool hands out next.
1002
+ *
1003
+ * Reads only, and the distinction is the whole design. For a write, "the
1004
+ * connection died" does not say whether the statement committed first: a
1005
+ * connection can drop after the server commits and before the reply
1006
+ * arrives, so resending an INSERT can insert it twice. A read has no such
1007
+ * outcome. Nor inside a transaction, where the connection that died WAS the
1008
+ * transaction and a retry on another one would run outside it.
1009
+ *
1010
+ * The loop turn is what makes a second attempt worth making: pg-pool evicts
1011
+ * an idle connection whose close it has read, so after one poll phase every
1012
+ * other connection that died alongside this one is gone from the idle list
1013
+ * rather than lent out to the retry.
1014
+ *
1015
+ * Every read call site uses this and every write uses `queryWithTimeout`
1016
+ * directly; `src/test/stale-connection-retry.test.ts` pins which is which.
1017
+ */
1018
+ private readWithTimeout;
996
1019
  /**
997
1020
  * Execute a pool.query with an optional timeout.
998
1021
  * If timeout is set, races the query against a timer and rejects on expiry.
999
1022
  * pg driver errors are translated to typed Turbine errors via wrapPgError.
1023
+ * `retryIfStale` is {@link readWithTimeout}'s; nothing else sets it.
1000
1024
  */
1001
1025
  private queryWithTimeout;
1002
1026
  /**
@@ -1013,11 +1037,18 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
1013
1037
  * write and a follow-up SELECT; the SELECT's rows feed the transform.
1014
1038
  */
1015
1039
  private executeMutation;
1016
- buildCreate(args: CreateArgs<T>): DeferredQuery<T>;
1040
+ buildCreate<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: CreateArgs<T, R, S, O>): DeferredQuery<FieldResult<T, S, O>>;
1041
+ /**
1042
+ * A single-row write's `select` / `omit`, resolved through the SAME
1043
+ * `resolveProjection` reads use (see WriteProjection in writes.ts), or
1044
+ * `undefined` for the default return shape. Resolved here rather than in
1045
+ * writes.ts because relations.ts imports writes.ts.
1046
+ */
1047
+ private resolveWriteProjection;
1017
1048
  buildCreateMany(args: CreateManyArgs<T>): DeferredQuery<T[]>;
1018
- buildUpdate(args: UpdateArgs<T>): DeferredQuery<T>;
1019
- buildDelete(args: DeleteArgs<T>): DeferredQuery<T>;
1020
- buildUpsert(args: UpsertArgs<T>): DeferredQuery<T>;
1049
+ buildUpdate<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: UpdateArgs<T, R, S, O>): DeferredQuery<FieldResult<T, S, O>>;
1050
+ buildDelete<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: DeleteArgs<T, R, S, O>): DeferredQuery<FieldResult<T, S, O>>;
1051
+ buildUpsert<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: UpsertArgs<T, R, S, O>): DeferredQuery<FieldResult<T, S, O>>;
1021
1052
  buildUpdateMany(args: UpdateManyArgs<T>): DeferredQuery<{
1022
1053
  count: number;
1023
1054
  }>;
@@ -1235,34 +1266,23 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
1235
1266
  buildFindFirstOrThrow<W extends TypedWithClause<R> = {}, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args?: FindManyArgs<T, R, W, S, O>): DeferredQuery<QueryResult<T, R, W, S, O>>;
1236
1267
  findUniqueOrThrow<W extends TypedWithClause<R> = {}, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: FindUniqueArgs<T, R, W, S, O>): Promise<QueryResult<T, R, W, S, O>>;
1237
1268
  buildFindUniqueOrThrow<W extends TypedWithClause<R> = {}, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: FindUniqueArgs<T, R, W, S, O>): DeferredQuery<QueryResult<T, R, W, S, O>>;
1238
- create(args: CreateArgs<T, R>): Promise<T>;
1269
+ create<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: CreateArgs<T, R, S, O>): Promise<FieldResult<T, S, O>>;
1239
1270
  createMany(args: CreateManyArgs<T>): Promise<T[]>;
1240
- update(args: UpdateArgs<T, R>): Promise<T>;
1271
+ update<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: UpdateArgs<T, R, S, O>): Promise<FieldResult<T, S, O>>;
1241
1272
  private nestedCreate;
1242
1273
  private nestedUpdate;
1243
1274
  /**
1244
- * Check out a pooled connection, translating a driver failure into a typed
1245
- * Turbine error.
1246
- *
1247
- * `pool.connect()` is where the first-run failures land: wrong password
1248
- * (SQLSTATE 28P01), no such database (3D000), nothing listening
1249
- * (ECONNREFUSED), an unverifiable TLS certificate. Unwrapped, every one of
1250
- * those surfaces from an ordinary `db.users.create({ data: { ...nested } })`
1251
- * as a raw pg `DatabaseError` whose `.code` is a SQLSTATE, on the same
1252
- * property Turbine puts `TURBINE_E0NN` in.
1253
- *
1254
- * client.ts has its own copy for `$transaction` / `connect()`; this one
1255
- * exists because `query/` must not import client.ts (circular dependency).
1256
- * The query paths need no equivalent: `pool.query()` opens the connection
1257
- * itself and rejects with the connect error, which the query boundary
1258
- * already wraps.
1275
+ * Check out a guarded pooled connection, as a typed error when that fails.
1276
+ * Shared with client.ts through checkout.ts (see there for why each part
1277
+ * matters); the cursor stream holds it, and a nested write opens its
1278
+ * implicit transaction on one through `openCheckout`.
1259
1279
  */
1260
1280
  private acquireConnection;
1261
1281
  private runInImplicitTx;
1262
1282
  private buildNestedCtx;
1263
1283
  private makeTxProxy;
1264
- delete(args: DeleteArgs<T, R>): Promise<T>;
1265
- upsert(args: UpsertArgs<T, R>): Promise<T>;
1284
+ delete<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: DeleteArgs<T, R, S, O>): Promise<FieldResult<T, S, O>>;
1285
+ upsert<S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined>(args: UpsertArgs<T, R, S, O>): Promise<FieldResult<T, S, O>>;
1266
1286
  updateMany(args: UpdateManyArgs<T, R>): Promise<{
1267
1287
  count: number;
1268
1288
  }>;