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/cjs/powql.js CHANGED
@@ -71,6 +71,7 @@ var __importStar = (this && this.__importStar) || (function () {
71
71
  Object.defineProperty(exports, "__esModule", { value: true });
72
72
  exports.PowqlInterface = exports.MAX_POWQL_DATETIME_TERMS = void 0;
73
73
  const node_crypto_1 = require("node:crypto");
74
+ const connection_guard_js_1 = require("./connection-guard.js");
74
75
  const errors_js_1 = require("./errors.js");
75
76
  const nested_write_js_1 = require("./nested-write.js");
76
77
  const powdb_shared_js_1 = require("./powdb-shared.js");
@@ -1272,6 +1273,34 @@ class PowqlInterface {
1272
1273
  }
1273
1274
  return entity;
1274
1275
  }
1276
+ /**
1277
+ * A single-row write's `select` / `omit`, resolved by {@link projectionPlan}
1278
+ * so the rules and messages are the read path's, and the SQL engines' (an
1279
+ * unknown name is E003, `select` must name something, the pair is refused).
1280
+ * `undefined` for the default return shape. Resolved BEFORE the write is sent,
1281
+ * so a bad projection writes nothing.
1282
+ *
1283
+ * The narrowing itself happens on the returned row ({@link shapeWriteRow}):
1284
+ * PowQL's `returning` takes no column list (see {@link stripWritePii}), so
1285
+ * unlike the SQL engines the unselected columns still cross the wire here.
1286
+ * The RESULT is identical across engines; the byte saving is SQL-only.
1287
+ */
1288
+ writeReturnPlan(args) {
1289
+ if (args.select === undefined && args.omit === undefined)
1290
+ return undefined;
1291
+ return this.projectionPlan(args.select, args.omit, false);
1292
+ }
1293
+ /** Apply a write's return plan to its row, or the default PII strip without one. */
1294
+ shapeWriteRow(entity, plan) {
1295
+ if (!entity || !plan)
1296
+ return this.stripWritePii(entity);
1297
+ const keep = new Set(plan.cols.filter((c) => !plan.forcedPk.includes(c)).map((c) => this.meta.reverseColumnMap[c] ?? c));
1298
+ for (const key of Object.keys(entity)) {
1299
+ if (!keep.has(key))
1300
+ delete entity[key];
1301
+ }
1302
+ return entity;
1303
+ }
1275
1304
  /** `{ .c1, .c2, … }` projection clause. */
1276
1305
  projection(cols) {
1277
1306
  return `{ ${cols.map((c) => this.colRefName(c)).join(', ')} }`;
@@ -2712,6 +2741,7 @@ class PowqlInterface {
2712
2741
  }
2713
2742
  async create(args) {
2714
2743
  return this.withMiddleware('create', args, async () => {
2744
+ const plan = this.writeReturnPlan(args);
2715
2745
  if ((0, nested_write_js_1.hasRelationFields)(args.data, this.meta)) {
2716
2746
  return this.nestedCreate(args);
2717
2747
  }
@@ -2723,7 +2753,7 @@ class PowqlInterface {
2723
2753
  .join(', ');
2724
2754
  // `returning` surfaces the inserted row (all columns, schema order) in one round-trip.
2725
2755
  const { rows, native } = await this.exec(`insert ${this.qt} { ${body} } returning`, params, args.timeout, 'create');
2726
- const row = rows.length ? this.stripWritePii(this.shape(rows, native)[0]) : null;
2756
+ const row = rows.length ? this.shapeWriteRow(this.shape(rows, native)[0], plan) : null;
2727
2757
  if (!row)
2728
2758
  throw new errors_js_1.NotFoundError({ table: this.table, where: data });
2729
2759
  return row;
@@ -2798,6 +2828,7 @@ class PowqlInterface {
2798
2828
  }
2799
2829
  async update(args) {
2800
2830
  return this.withMiddleware('update', args, async () => {
2831
+ const plan = this.writeReturnPlan(args);
2801
2832
  if ((0, nested_write_js_1.hasRelationFields)(args.data, this.meta)) {
2802
2833
  // The nested engine re-enters `update` / `findUnique` for the parent
2803
2834
  // row before it writes anything, so the identity rule below still runs
@@ -2835,7 +2866,7 @@ class PowqlInterface {
2835
2866
  }
2836
2867
  // `returning` hands back the post-update row(s); take the first (single-row contract).
2837
2868
  const { rows, native } = await this.exec(`${this.qt} filter ${where} update { ${setClause} } returning`, params, args.timeout, 'update');
2838
- const row = rows.length ? this.stripWritePii(this.shape(rows, native)[0]) : null;
2869
+ const row = rows.length ? this.shapeWriteRow(this.shape(rows, native)[0], plan) : null;
2839
2870
  if (!row) {
2840
2871
  if (lock) {
2841
2872
  throw new errors_js_1.OptimisticLockError({
@@ -2912,18 +2943,21 @@ class PowqlInterface {
2912
2943
  }
2913
2944
  async nestedCreate(args) {
2914
2945
  const data = args.data;
2946
+ // Applied by the nested engine's final read-back, as on the SQL engines.
2947
+ const shape = { select: args.select, omit: args.omit };
2915
2948
  if (this.isTxScoped()) {
2916
- return (0, nested_write_js_1.executeNestedCreate)(this.buildNestedCtx(), this.table, data);
2949
+ return (0, nested_write_js_1.executeNestedCreate)(this.buildNestedCtx(), this.table, data, 0, [], shape);
2917
2950
  }
2918
- return this.runInImplicitTx((ctx) => (0, nested_write_js_1.executeNestedCreate)(ctx, this.table, data));
2951
+ return this.runInImplicitTx((ctx) => (0, nested_write_js_1.executeNestedCreate)(ctx, this.table, data, 0, [], shape));
2919
2952
  }
2920
2953
  async nestedUpdate(args) {
2921
2954
  const data = args.data;
2922
2955
  const where = args.where;
2956
+ const shape = { select: args.select, omit: args.omit };
2923
2957
  if (this.isTxScoped()) {
2924
- return (0, nested_write_js_1.executeNestedUpdate)(this.buildNestedCtx(), this.table, where, data);
2958
+ return (0, nested_write_js_1.executeNestedUpdate)(this.buildNestedCtx(), this.table, where, data, 0, [], shape);
2925
2959
  }
2926
- return this.runInImplicitTx((ctx) => (0, nested_write_js_1.executeNestedUpdate)(ctx, this.table, where, data));
2960
+ return this.runInImplicitTx((ctx) => (0, nested_write_js_1.executeNestedUpdate)(ctx, this.table, where, data, 0, [], shape));
2927
2961
  }
2928
2962
  /** Open a flat PowDB transaction on a pinned connection and run `fn` inside it. */
2929
2963
  async runInImplicitTx(fn) {
@@ -2936,6 +2970,9 @@ class PowqlInterface {
2936
2970
  // drifts from `powdbDialect`; falls back to the literal lowercase keywords.
2937
2971
  const d = this.options.dialect;
2938
2972
  const client = await this.pool.connect();
2973
+ // A no-op today (PowDB checkouts have no event surface), kept so this path
2974
+ // cannot become the one unguarded checkout if a transport ever emits 'error'.
2975
+ const checkout = (0, connection_guard_js_1.guardCheckout)(client);
2939
2976
  let began = false;
2940
2977
  try {
2941
2978
  await client.query(d?.beginStatement?.() ?? 'begin');
@@ -2975,7 +3012,7 @@ class PowqlInterface {
2975
3012
  throw err;
2976
3013
  }
2977
3014
  finally {
2978
- client.release();
3015
+ checkout.release();
2979
3016
  }
2980
3017
  }
2981
3018
  /** Already inside a transaction: build a context whose table accessors reuse the pinned pool. */
@@ -2988,6 +3025,7 @@ class PowqlInterface {
2988
3025
  }
2989
3026
  async delete(args) {
2990
3027
  return this.withMiddleware('delete', args, async () => {
3028
+ const plan = this.writeReturnPlan(args);
2991
3029
  const allowFullTableScan = (0, types_js_1.resolveUnsafeFlag)(args.allowFullTableScan, 'allowFullTableScan');
2992
3030
  const userWhere = this.expandedWhere(args.where);
2993
3031
  const params = [];
@@ -2998,7 +3036,7 @@ class PowqlInterface {
2998
3036
  where = this.applyGlobalFilter(where, params, args.skipGlobalFilters);
2999
3037
  // `returning` hands back the deleted row(s), no separate pre-image reselect needed.
3000
3038
  const { rows, native } = await this.exec(`${this.qt} filter ${where} delete returning`, params, args.timeout, 'delete');
3001
- const row = rows.length ? this.stripWritePii(this.shape(rows, native)[0]) : null;
3039
+ const row = rows.length ? this.shapeWriteRow(this.shape(rows, native)[0], plan) : null;
3002
3040
  if (!row)
3003
3041
  throw new errors_js_1.NotFoundError({ table: this.table, where: args.where });
3004
3042
  return row;
@@ -3024,13 +3062,45 @@ class PowqlInterface {
3024
3062
  // disagreeing about whether a query is VALID, which is the failure class
3025
3063
  // 0.64.0 and 0.72.0 were both spent on. Before `applyPkDefault`, so a
3026
3064
  // refused upsert mints no client-side UUID and sends nothing.
3027
- this.assertMutationIdentifiesOneRow(this.expandedWhere(args.where), false, 'upsert');
3065
+ const upsertWhere = (this.expandedWhere(args.where) ?? {});
3066
+ this.assertMutationIdentifiesOneRow(upsertWhere, false, 'upsert');
3067
+ const plan = this.writeReturnPlan(args);
3028
3068
  const createData = this.applyPkDefault(args.create);
3029
3069
  const pkCol = this.meta.primaryKey[0];
3030
- if (this.meta.primaryKey.length !== 1 || !pkCol) {
3031
- // PowQL's native `upsert … on .col` takes a single conflict column, so a
3032
- // composite PK falls back to an atomic reselect-or-write transaction.
3033
- return this.upsertComposite(createData, args.update);
3070
+ // The conflict target is the columns `where` names, as on the SQL
3071
+ // engines (`ON CONFLICT (<where keys>)`), with the conflicting VALUES
3072
+ // taken from `create`. This path used to conflict on the primary key
3073
+ // whatever `where` named, so an upsert keyed on another unique column
3074
+ // (`where: { email }`) never found the existing row.
3075
+ const conflictColumns = Object.keys(upsertWhere)
3076
+ .filter((k) => upsertWhere[k] !== undefined)
3077
+ .map((k) => this.column(k).name);
3078
+ // PowQL's native `upsert … on .col` expresses exactly one shape: a single
3079
+ // conflict column that is the primary key, with no predicate on its
3080
+ // conflict branch. Everything else is an atomic reselect-or-write
3081
+ // transaction.
3082
+ //
3083
+ // That includes a table under a global filter. The native statement's
3084
+ // `on conflict` branch carries no predicate, so a tenant-scoped client
3085
+ // whose `create` key matched ANOTHER tenant's row updated that row, and
3086
+ // the tenant-filtered reselect then reported the write it had just made
3087
+ // as a NotFoundError. The SQL engines guard the same branch with the
3088
+ // filter (`DO UPDATE … WHERE tenant_id = $n`). The lookup-first path runs
3089
+ // its find and its update through the filtered interface, so another
3090
+ // tenant's row is invisible to it: the insert then collides on the key
3091
+ // and fails, as it does on the SQL engines, and nothing is overwritten.
3092
+ const filtered = this.applyGlobalFilter('', [], args.skipGlobalFilters) !== '';
3093
+ const native = this.meta.primaryKey.length === 1 &&
3094
+ pkCol !== undefined &&
3095
+ conflictColumns.length === 1 &&
3096
+ conflictColumns[0] === pkCol &&
3097
+ !filtered;
3098
+ if (!native) {
3099
+ return this.upsertLookupFirst(createData, args.update, conflictColumns, {
3100
+ select: args.select,
3101
+ omit: args.omit,
3102
+ skipGlobalFilters: args.skipGlobalFilters,
3103
+ });
3034
3104
  }
3035
3105
  const params = [];
3036
3106
  const createBody = this.scalarData(createData)
@@ -3043,40 +3113,61 @@ class PowqlInterface {
3043
3113
  // reselect-by-PK fetch; create/update/delete all use `returning`.
3044
3114
  await this.exec(`upsert ${this.qt} on ${this.colRefName(pkCol)} { ${createBody} } on conflict { ${updateBody} }`, params, args.timeout, 'upsert');
3045
3115
  const pkField = this.meta.reverseColumnMap[pkCol] ?? pkCol;
3046
- // Either spelling, as `upsertComposite` already does below: reading the
3116
+ // Either spelling, as `upsertLookupFirst` already does below: reading the
3047
3117
  // camelCase field alone reselected `undefined` for a snake-spelled PK and
3048
3118
  // reported a write that had SUCCEEDED as a NotFoundError.
3049
- const row = await this.reselectByPk(createData[pkField] ?? createData[pkCol], args.timeout);
3119
+ // `args` carries skipGlobalFilters too: an upsert that opted out of the
3120
+ // filter must not have its own write filtered out of the reselect.
3121
+ const row = await this.reselectByPk(createData[pkField] ?? createData[pkCol], args.timeout, args);
3050
3122
  if (!row)
3051
3123
  throw new errors_js_1.NotFoundError({ table: this.table, where: createData });
3052
- return row;
3124
+ // Without a plan the reselect's default projection already left PII out.
3125
+ return plan ? this.shapeWriteRow(row, plan) : row;
3053
3126
  });
3054
3127
  }
3055
3128
  /**
3056
- * Composite-key upsert: PowQL's `upsert … on .col` only takes one conflict
3057
- * column, so reselect by the full composite PK and update-or-create inside one
3058
- * flat transaction (PowDB single-writer makes the read-then-write safe from
3059
- * concurrent writers; the transaction makes it atomic with the write).
3060
- */
3061
- async upsertComposite(createData, updateData) {
3129
+ * Upsert as a reselect-or-write inside one flat transaction, for every shape
3130
+ * the native `upsert … on .col` statement cannot express: a conflict target
3131
+ * other than a single-column primary key (it takes one column and PowDB has
3132
+ * no composite unique), and a table under a global filter (its conflict
3133
+ * branch takes no predicate). PowDB's single writer makes the read-then-write
3134
+ * safe from concurrent writers; the transaction makes it atomic with the
3135
+ * write.
3136
+ *
3137
+ * The row is looked up by the conflict columns with `create`'s values for
3138
+ * them, which is what `ON CONFLICT (<cols>)` compares on the SQL engines. The
3139
+ * find and the update run through the transaction's table interface, so a
3140
+ * configured global filter applies to both exactly as it does to any other
3141
+ * read or write, and `skipGlobalFilters` is forwarded to both.
3142
+ */
3143
+ async upsertLookupFirst(createData, updateData, conflictColumns, shape) {
3144
+ const { skipGlobalFilters, ...projection } = shape;
3145
+ const skip = skipGlobalFilters === undefined ? {} : { skipGlobalFilters };
3062
3146
  // Accept either the camelCase field or the snake_case column in `create`
3063
3147
  // (create() resolves both), and key the where by field name.
3064
- const pkPairs = this.meta.primaryKey.map((pk) => {
3065
- const field = this.meta.reverseColumnMap[pk] ?? pk;
3066
- return { field, value: createData[field] ?? createData[pk] };
3148
+ const keyPairs = conflictColumns.map((col) => {
3149
+ const field = this.meta.reverseColumnMap[col] ?? col;
3150
+ return { field, value: createData[field] ?? createData[col] };
3067
3151
  });
3068
- if (pkPairs.some((p) => p.value == null)) {
3069
- throw new errors_js_1.ValidationError(`upsert on "${this.table}" needs every composite-PK field in \`create\` (${pkPairs
3152
+ const keyless = keyPairs.some((p) => p.value == null);
3153
+ const isCompositePk = keyPairs.length > 1 &&
3154
+ keyPairs.length === this.meta.primaryKey.length &&
3155
+ conflictColumns.every((c) => this.meta.primaryKey.includes(c));
3156
+ if (keyless && isCompositePk) {
3157
+ throw new errors_js_1.ValidationError(`upsert on "${this.table}" needs every composite-PK field in \`create\` (${keyPairs
3070
3158
  .map((p) => p.field)
3071
3159
  .join(', ')}).`);
3072
3160
  }
3073
- const pkWhere = Object.fromEntries(pkPairs.map((p) => [p.field, p.value]));
3161
+ const keyWhere = Object.fromEntries(keyPairs.map((p) => [p.field, p.value]));
3074
3162
  const run = async (ctx) => {
3075
3163
  const tbl = ctx.tx.table(this.table);
3076
- const existing = await tbl.findUnique({ where: pkWhere });
3164
+ // A row whose `create` leaves a conflict column unset cannot collide on
3165
+ // it (NULL never equals NULL in a unique index, and a server-assigned key
3166
+ // is new), which is what `ON CONFLICT` concludes on the SQL engines too.
3167
+ const existing = keyless ? null : await tbl.findUnique({ where: keyWhere, ...skip });
3077
3168
  return existing
3078
- ? (await tbl.update({ where: pkWhere, data: updateData }))
3079
- : (await tbl.create({ data: createData }));
3169
+ ? (await tbl.update({ where: keyWhere, data: updateData, ...projection, ...skip }))
3170
+ : (await tbl.create({ data: createData, ...projection }));
3080
3171
  };
3081
3172
  return this.isTxScoped() ? run(this.buildNestedCtx()) : this.runInImplicitTx(run);
3082
3173
  }
@@ -3123,33 +3214,64 @@ class PowqlInterface {
3123
3214
  'every SQL engine exactly when the column is nullable. `_count: true` (a row count) and a per-field ' +
3124
3215
  '`_count` of a NOT NULL column are correct on every version and are never refused.');
3125
3216
  }
3217
+ /**
3218
+ * Can `aggregate()` compute several aggregates in ONE statement on this engine?
3219
+ *
3220
+ * PowQL refuses a bare multi-aggregate projection (`T { a: sum(.x), b: sum(.y) }`
3221
+ * is "aggregate function in an unsupported position" on every engine version),
3222
+ * so the only one-statement form is a grouping over a literal key,
3223
+ * `T filter … group 1 { agg_0: sum(.x), agg_1: sum(.y) }`, which yields a
3224
+ * single group holding every row the filter matched. Measured against the
3225
+ * embedded addon at 0.7.1 through 0.28: below 0.13 a literal group key does not
3226
+ * parse, from 0.13 to 0.19.1 the grouped per-field `count` disagrees with the
3227
+ * scalar `count(T { .col })` on a nullable column, and from 0.20 every
3228
+ * aggregate kind (count / sum / avg / min / max over int, float, str, an
3229
+ * all-null column, and several filters) answers identically on both wires.
3230
+ *
3231
+ * Derived from the PROBED engine version rather than from a capability flag,
3232
+ * so an unprobed pool (`engineVersion: null`, e.g. an injected pool carrying
3233
+ * {@link ALL_POWDB_CAPABILITIES}) keeps the per-field statements: this changes
3234
+ * the emitted PowQL, and an older engine would reject it outright, the same
3235
+ * probe-only discipline `nestedProjections` follows.
3236
+ */
3237
+ get groupsAggregatesInOneStatement() {
3238
+ const sem = (0, powdb_shared_js_1.parsePowdbSemver)(this.capabilities.engineVersion);
3239
+ return sem !== null && (0, powdb_shared_js_1.atLeastVersion)(sem, 0, 20);
3240
+ }
3126
3241
  async aggregate(args) {
3127
3242
  this.assertNoForceCustomPlan(args);
3128
3243
  return this.withMiddleware('aggregate', args, async () => {
3129
- // One scalar query per aggregate, PowDB's bare-projection aggregate is broken.
3130
3244
  const result = {};
3131
3245
  const filterParams = [];
3132
3246
  const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
3133
3247
  let where = this.buildWhere(resolvedWhere, filterParams);
3134
3248
  where = this.applyGlobalFilter(where, filterParams, args.skipGlobalFilters);
3135
3249
  const filter = where ? ` filter ${where}` : '';
3136
- const scalar = async (expr) => {
3137
- const params = [...filterParams];
3138
- const { rows } = await this.exec(expr, params, args.timeout, 'aggregate');
3139
- const v = rows[0]?.value;
3140
- return v == null || v === 'null' ? null : Number(v);
3141
- };
3250
+ const plan = [];
3142
3251
  if (args._count) {
3143
3252
  if (args._count === true) {
3144
- result._count = (await scalar(`count(${this.qt}${filter})`)) ?? 0;
3253
+ plan.push({
3254
+ scalar: `count(${this.qt}${filter})`,
3255
+ inner: 'count(*)',
3256
+ assign: (v) => {
3257
+ result._count = v ?? 0;
3258
+ },
3259
+ });
3145
3260
  }
3146
3261
  else {
3147
3262
  const counts = {};
3263
+ result._count = counts;
3148
3264
  for (const field of Object.keys(args._count).filter((f) => args._count[f])) {
3149
3265
  this.assertProjectedCountSupported(field);
3150
- counts[field] = (await scalar(`count(${this.qt}${filter} { ${this.ref(field)} })`)) ?? 0;
3266
+ const ref = this.ref(field);
3267
+ plan.push({
3268
+ scalar: `count(${this.qt}${filter} { ${ref} })`,
3269
+ inner: `count(${ref})`,
3270
+ assign: (v) => {
3271
+ counts[field] = v ?? 0;
3272
+ },
3273
+ });
3151
3274
  }
3152
- result._count = counts;
3153
3275
  }
3154
3276
  }
3155
3277
  for (const fn of ['_sum', '_avg', '_min', '_max']) {
@@ -3157,15 +3279,44 @@ class PowqlInterface {
3157
3279
  if (!spec)
3158
3280
  continue;
3159
3281
  const acc = {};
3282
+ result[fn] = acc;
3283
+ const powfn = fn.slice(1); // sum/avg/min/max
3160
3284
  for (const field of Object.keys(spec).filter((f) => spec[f])) {
3161
- const powfn = fn.slice(1); // sum/avg/min/max
3162
3285
  // Same PII contract as the SQL engines: _min/_max return a stored cell.
3163
3286
  if (fn === '_min' || fn === '_max') {
3164
3287
  (0, aggregates_js_1.assertAggregatePiiOptIn)(this.table, this.meta, field, this.column(field).name, `aggregate ${fn}`, (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii'));
3165
3288
  }
3166
- acc[field] = await scalar(`${powfn}(${this.qt}${filter} { ${this.ref(field)} })`);
3289
+ const ref = this.ref(field);
3290
+ plan.push({
3291
+ scalar: `${powfn}(${this.qt}${filter} { ${ref} })`,
3292
+ inner: `${powfn}(${ref})`,
3293
+ assign: (v) => {
3294
+ acc[field] = v;
3295
+ },
3296
+ });
3167
3297
  }
3168
- result[fn] = acc;
3298
+ }
3299
+ // A scalar cell and a grouped cell decode through the same wire decoder,
3300
+ // so one coercion serves both forms.
3301
+ const toNumber = (v) => (v == null || v === 'null' ? null : Number(v));
3302
+ if (plan.length > 1 && this.groupsAggregatesInOneStatement) {
3303
+ const proj = plan.map((p, i) => `agg_${i}: ${p.inner}`).join(', ');
3304
+ const { rows } = await this.exec(`${this.qt}${filter} group 1 { ${proj} }`, [...filterParams], args.timeout, 'aggregate');
3305
+ // Zero groups means the filter matched no row. The per-field statements
3306
+ // below answer that case instead of a synthesized one, because what an
3307
+ // aggregate over NO rows returns is engine-versioned (`sum` answered 0
3308
+ // before PowDB 0.28 and null since), and the scalar form is the one
3309
+ // whose answer this method has always returned.
3310
+ const row = rows.length === 1 ? rows[0] : undefined;
3311
+ if (row) {
3312
+ for (const [i, p] of plan.entries())
3313
+ p.assign(toNumber(row[`agg_${i}`]));
3314
+ return result;
3315
+ }
3316
+ }
3317
+ for (const p of plan) {
3318
+ const { rows } = await this.exec(p.scalar, [...filterParams], args.timeout, 'aggregate');
3319
+ p.assign(toNumber(rows[0]?.value));
3169
3320
  }
3170
3321
  return result;
3171
3322
  });
@@ -3568,12 +3719,15 @@ class PowqlInterface {
3568
3719
  // Reselect helper (upsert only, PowDB's upsert has no `returning`)
3569
3720
  // -------------------------------------------------------------------------
3570
3721
  /** Reselect a single row by its single-column primary key value. */
3571
- async reselectByPk(pkValue, timeout) {
3722
+ async reselectByPk(pkValue, timeout, shape) {
3572
3723
  const pkField = this.meta.reverseColumnMap[this.meta.primaryKey[0]] ?? this.meta.primaryKey[0];
3573
3724
  const { rows, native } = await this.runFind({
3574
3725
  where: { [pkField]: pkValue },
3575
3726
  limit: 1,
3576
3727
  timeout,
3728
+ select: shape?.select,
3729
+ omit: shape?.omit,
3730
+ ...(shape?.skipGlobalFilters === undefined ? {} : { skipGlobalFilters: shape.skipGlobalFilters }),
3577
3731
  });
3578
3732
  return rows.length ? this.shape(rows, native)[0] : null;
3579
3733
  }
@@ -1005,6 +1005,22 @@ function resolveWriteProjection(ctx, mm, op, args) {
1005
1005
  }
1006
1006
  return NO_WRITE_PROJECTION;
1007
1007
  }
1008
+ /**
1009
+ * The same projection in core's spelling, for the write itself. Passed DOWN so
1010
+ * the SQL engines narrow the RETURNING list rather than fetching the whole row
1011
+ * for {@link applyWriteProjection} to trim afterwards, which on a table with a
1012
+ * large JSON column was most of the bytes the write moved. The trim stays: it
1013
+ * is what keeps the result Prisma-shaped on every path, including the
1014
+ * lookup-first upsert and PowDB, whose `returning` takes no column list.
1015
+ */
1016
+ function coreWriteProjection(mm, proj) {
1017
+ const spell = (keys) => Object.fromEntries([...keys].map((k) => [renameField(mm, k), true]));
1018
+ if (proj.pick)
1019
+ return { select: spell(proj.pick) };
1020
+ if (proj.omit)
1021
+ return { omit: spell(proj.omit) };
1022
+ return {};
1023
+ }
1008
1024
  /** Apply a {@link WriteProjection} to one already-reshaped write result row. */
1009
1025
  function applyWriteProjection(proj, row) {
1010
1026
  if (!isPlainObject(row))
@@ -1799,9 +1815,12 @@ function upsertKeysMatch(t) {
1799
1815
  /** Prisma upsert semantics: look up by where; update the found row, else insert create. */
1800
1816
  async function upsertLookupFirst(qi, t) {
1801
1817
  const existing = await qi.findUnique({ where: t.where });
1818
+ // The projection rides along so both branches return what the native upsert
1819
+ // path returns (an explicitly selected PII field included).
1820
+ const shape = { select: t.select, omit: t.omit };
1802
1821
  if (existing)
1803
- return qi.update({ where: t.where, data: t.update });
1804
- return qi.create({ data: t.create });
1822
+ return qi.update({ where: t.where, data: t.update, ...shape });
1823
+ return qi.create({ data: t.create, ...shape });
1805
1824
  }
1806
1825
  /**
1807
1826
  * The row shapes a translated `createMany` has to insert, as contiguous runs
@@ -1909,7 +1928,10 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
1909
1928
  const shape = (raw) => applyWriteProjection(proj, reshapeRow(ctx, mm, raw));
1910
1929
  return defer('create', args, () => {
1911
1930
  proj = resolveWriteProjection(ctx, mm, 'create', args);
1912
- const t = { data: translateWriteData(ctx, mm, applyCreateDefaults(mm, args.data)) };
1931
+ const t = {
1932
+ data: translateWriteData(ctx, mm, applyCreateDefaults(mm, args.data)),
1933
+ ...coreWriteProjection(mm, proj),
1934
+ };
1913
1935
  (0, index_js_1.applyNativeOptions)(index_js_1.CREATE_OPTIONS, args, t);
1914
1936
  return t;
1915
1937
  }, (qi, t) => qi.create(t).then(shape), {
@@ -1954,6 +1976,7 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
1954
1976
  const t = {
1955
1977
  where: translateWhere(ctx, mm, a.where),
1956
1978
  data: translateWriteData(ctx, mm, applyUpdateTouch(mm, a.data)),
1979
+ ...coreWriteProjection(mm, proj),
1957
1980
  };
1958
1981
  (0, index_js_1.applyNativeOptions)(index_js_1.UPDATE_OPTIONS, a, t);
1959
1982
  // `optimisticLock.field` is a FIELD NAME, so it is renamed rather than
@@ -1993,7 +2016,7 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
1993
2016
  return defer('delete', args, () => {
1994
2017
  const a = requireWhere(args, 'delete');
1995
2018
  proj = resolveWriteProjection(ctx, mm, 'delete', a);
1996
- const t = { where: translateWhere(ctx, mm, a.where) };
2019
+ const t = { where: translateWhere(ctx, mm, a.where), ...coreWriteProjection(mm, proj) };
1997
2020
  (0, index_js_1.applyNativeOptions)(index_js_1.DELETE_OPTIONS, a, t);
1998
2021
  return t;
1999
2022
  }, (qi, t) => qi.delete(t).then(shape), { build: (qi, t) => qi.buildDelete(t), reshape: shape });
@@ -2018,6 +2041,7 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
2018
2041
  where: translateWhere(ctx, mm, a.where),
2019
2042
  create: translateWriteData(ctx, mm, applyCreateDefaults(mm, a.create)),
2020
2043
  update: translateWriteData(ctx, mm, applyUpdateTouch(mm, a.update)),
2044
+ ...coreWriteProjection(mm, proj),
2021
2045
  };
2022
2046
  (0, index_js_1.applyNativeOptions)(index_js_1.UPSERT_OPTIONS, a, t);
2023
2047
  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
  }>;