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
@@ -44,22 +44,22 @@ export declare function coerceWriteValue(qi: BuilderCtx, key: string, value: unk
44
44
  * return rows from non-RETURNING engines. Reuses the same parameterized WHERE
45
45
  * builder as reads, so no user value is interpolated.
46
46
  */
47
- export declare function buildReselectByWhere(qi: BuilderCtx, whereObj: Record<string, unknown>): {
47
+ export declare function buildReselectByWhere(qi: BuilderCtx, whereObj: Record<string, unknown>, projection?: WriteProjection): {
48
48
  sql: string;
49
49
  params: unknown[];
50
50
  };
51
- export declare function buildCreate<T extends object>(qi: BuilderCtx, args: CreateArgs<T>): DeferredQuery<T>;
51
+ export declare function buildCreate<T extends object>(qi: BuilderCtx, args: CreateArgs<T>, projection?: WriteProjection): DeferredQuery<T>;
52
52
  /**
53
53
  * Build the `'reselect'` plan for {@link buildCreate}: run the INSERT, then
54
54
  * `SELECT * WHERE pk = ?`. Returns `undefined` (skipped) unless the active
55
55
  * dialect's result strategy is `'reselect'`, so the PostgreSQL/RETURNING path
56
56
  * pays nothing. Not yet wired to a real non-RETURNING engine.
57
57
  */
58
- export declare function makeCreateReselect<T extends object>(qi: BuilderCtx, insertSql: string, insertParams: unknown[], data: Record<string, unknown>): DeferredQuery<T>['reselect'];
58
+ export declare function makeCreateReselect<T extends object>(qi: BuilderCtx, insertSql: string, insertParams: unknown[], data: Record<string, unknown>, projection?: WriteProjection): DeferredQuery<T>['reselect'];
59
59
  export declare function buildCreateMany<T extends object>(qi: BuilderCtx, args: CreateManyArgs<T>): DeferredQuery<T[]>;
60
- export declare function buildUpdate<T extends object>(qi: BuilderCtx, args: UpdateArgs<T>): DeferredQuery<T>;
61
- export declare function buildDelete<T extends object>(qi: BuilderCtx, args: DeleteArgs<T>): DeferredQuery<T>;
62
- export declare function buildUpsert<T extends object>(qi: BuilderCtx, args: UpsertArgs<T>): DeferredQuery<T>;
60
+ export declare function buildUpdate<T extends object>(qi: BuilderCtx, args: UpdateArgs<T>, projection?: WriteProjection): DeferredQuery<T>;
61
+ export declare function buildDelete<T extends object>(qi: BuilderCtx, args: DeleteArgs<T>, projection?: WriteProjection): DeferredQuery<T>;
62
+ export declare function buildUpsert<T extends object>(qi: BuilderCtx, args: UpsertArgs<T>, projection?: WriteProjection): DeferredQuery<T>;
63
63
  export declare function buildUpdateMany<T extends object>(qi: BuilderCtx, args: UpdateManyArgs<T>): DeferredQuery<{
64
64
  count: number;
65
65
  }>;
@@ -83,6 +83,36 @@ export declare function piiColumns(_qi: BuilderCtx, meta: TableMetadata): Set<st
83
83
  * exclusion; you may still write PII fields freely).
84
84
  */
85
85
  export declare function piiFields(_qi: BuilderCtx, meta: TableMetadata): string[];
86
+ /**
87
+ * A single-row write's caller-chosen return shape, from its `select` / `omit`.
88
+ *
89
+ * Resolved ONCE per call by builder.ts through `resolveProjection`, the same
90
+ * authority reads use, so a write and a read agree on every rule: a name must
91
+ * resolve to a column or the call throws E003, a relation name gets its own
92
+ * message, `select` must name something, `select` and `omit` are exclusive, the
93
+ * list is in TABLE order (a caller's key order must not mint distinct
94
+ * statements), and an explicit `select` of a PII column is the opt-in that
95
+ * returns it while `omit` leaves PII excluded. (builder.ts rather than this
96
+ * module because relations.ts already imports this one.)
97
+ *
98
+ * The point is bytes: `RETURNING *` sends back every column, a large JSON
99
+ * payload included, on every insert. Measured with a 3.1 KB jsonb column at
100
+ * concurrency 50, the same INSERT ran at 60,900/s with no RETURNING and at
101
+ * 34,500/s with `RETURNING *`.
102
+ */
103
+ export interface WriteProjection {
104
+ /** Unquoted column names, in table order. Never empty. */
105
+ readonly columns: readonly string[];
106
+ }
107
+ /**
108
+ * The write-SQL cache segment for a projection. EMPTY for the default shape, so
109
+ * every existing key is byte-identical; a projected statement differs only in
110
+ * its RETURNING/OUTPUT text, and without this segment two calls with the same
111
+ * SET and WHERE but different `select`s would share one cached statement, i.e.
112
+ * the second caller would get the first caller's columns. NUL is the delimiter
113
+ * because it is the one byte a PostgreSQL identifier cannot contain.
114
+ */
115
+ export declare function writeProjectionCacheSegment(projection: WriteProjection | undefined): string;
86
116
  /**
87
117
  * The `RETURNING` / `OUTPUT` selection for a write on this table. A table with
88
118
  * no PII column returns `'*'` (every column, byte-identical SQL to before);
@@ -119,13 +149,13 @@ export declare function piiFields(_qi: BuilderCtx, meta: TableMetadata): string[
119
149
  * time (`SET "updated_at" = $1, "updated_at" = $2`, PostgreSQL 42701).
120
150
  */
121
151
  export declare function applyUpdatedAtColumns(qi: BuilderCtx, data: Record<string, unknown>): Record<string, unknown>;
122
- export declare function writeReturningColumns(qi: BuilderCtx): ReturningSelection;
152
+ export declare function writeReturningColumns(qi: BuilderCtx, projection?: WriteProjection): ReturningSelection;
123
153
  /**
124
154
  * String form of {@link writeReturningColumns} for a `SELECT` list (the
125
155
  * `'reselect'` result strategy re-fetches via a SELECT, not RETURNING).
126
156
  * `'*'` when there is no PII column; otherwise the comma-joined quoted list.
127
157
  */
128
- export declare function writeReselectSelection(qi: BuilderCtx): string;
158
+ export declare function writeReselectSelection(qi: BuilderCtx, projection?: WriteProjection): string;
129
159
  /**
130
160
  * Parse a write's returned row (create/update/upsert/delete), then strip the
131
161
  * table's PII fields: the write-side read policy. On PII-tagged tables the
@@ -134,7 +164,7 @@ export declare function writeReselectSelection(qi: BuilderCtx): string;
134
164
  * no-op. Untagged tables incur only one `for` over a zero-length field list,
135
165
  * so behavior is unchanged.
136
166
  */
137
- export declare function parseWriteRow(qi: BuilderCtx, row: Record<string, unknown>): Record<string, unknown>;
167
+ export declare function parseWriteRow(qi: BuilderCtx, row: Record<string, unknown>, projection?: WriteProjection): Record<string, unknown>;
138
168
  /**
139
169
  * Reject any write against a view (H4). Views are introspected with
140
170
  * `isView: true` and are read-only in every engine; a write raises a
@@ -57,6 +57,7 @@ exports.buildUpdateMany = buildUpdateMany;
57
57
  exports.buildDeleteMany = buildDeleteMany;
58
58
  exports.piiColumns = piiColumns;
59
59
  exports.piiFields = piiFields;
60
+ exports.writeProjectionCacheSegment = writeProjectionCacheSegment;
60
61
  exports.applyUpdatedAtColumns = applyUpdatedAtColumns;
61
62
  exports.writeReturningColumns = writeReturningColumns;
62
63
  exports.writeReselectSelection = writeReselectSelection;
@@ -133,11 +134,11 @@ function utcDateTimeWrites(qi) {
133
134
  * return rows from non-RETURNING engines. Reuses the same parameterized WHERE
134
135
  * builder as reads, so no user value is interpolated.
135
136
  */
136
- function buildReselectByWhere(qi, whereObj) {
137
+ function buildReselectByWhere(qi, whereObj, projection) {
137
138
  const params = [];
138
139
  const clause = whereMod.buildWhereClause(qi, whereObj, params);
139
140
  const where = clause ? ` WHERE ${clause}` : '';
140
- return { sql: `SELECT ${writeReselectSelection(qi)} FROM ${qi.q(qi.table)}${where}`, params };
141
+ return { sql: `SELECT ${writeReselectSelection(qi, projection)} FROM ${qi.q(qi.table)}${where}`, params };
141
142
  }
142
143
  /**
143
144
  * Build the all-defaults INSERT for a `data` that names no column, via the
@@ -157,7 +158,7 @@ function buildReselectByWhere(qi, whereObj) {
157
158
  * A dialect predating the hook raises E017 rather than emitting SQL its engine
158
159
  * will reject.
159
160
  */
160
- function buildDefaultValuesInsert(qi, rowCount, skipDuplicates) {
161
+ function buildDefaultValuesInsert(qi, rowCount, skipDuplicates, projection) {
161
162
  const build = qi.dialect.buildDefaultValuesInsertStatement;
162
163
  if (!build) {
163
164
  throw new errors_js_1.UnsupportedFeatureError('create/createMany with an empty data object', qi.dialect.name, 'This dialect has no all-defaults INSERT form; name at least one column in `data`.');
@@ -166,10 +167,10 @@ function buildDefaultValuesInsert(qi, rowCount, skipDuplicates) {
166
167
  table: qi.q(qi.table),
167
168
  rowCount,
168
169
  skipDuplicates,
169
- returning: writeReturningColumns(qi),
170
+ returning: writeReturningColumns(qi, projection),
170
171
  });
171
172
  }
172
- function buildCreate(qi, args) {
173
+ function buildCreate(qi, args, projection) {
173
174
  assertWritable(qi, 'create');
174
175
  assertNoGeneratedColumns(qi, args.data, 'create');
175
176
  const entries = writeEntries(qi, args.data);
@@ -179,12 +180,12 @@ function buildCreate(qi, args) {
179
180
  const placeholders = entries.map(([k], i) => `${qi.p(i + 1)}${whereMod.enumCastSuffix(qi, qi.toColumn(k))}`);
180
181
  // `data: {}` (or all-undefined) names no column: insert a row of defaults.
181
182
  const sql = entries.length === 0
182
- ? buildDefaultValuesInsert(qi, 1)
183
+ ? buildDefaultValuesInsert(qi, 1, undefined, projection)
183
184
  : qi.dialect.buildInsertStatement({
184
185
  table: qi.q(qi.table),
185
186
  columns,
186
187
  valuePlaceholders: placeholders,
187
- returning: writeReturningColumns(qi),
188
+ returning: writeReturningColumns(qi, projection),
188
189
  });
189
190
  return {
190
191
  sql,
@@ -198,12 +199,12 @@ function buildCreate(qi, args) {
198
199
  message: `create on "${qi.table}" returned no row from RETURNING *; this should never happen.`,
199
200
  });
200
201
  }
201
- return parseWriteRow(qi, row);
202
+ return parseWriteRow(qi, row, projection);
202
203
  },
203
204
  tag: `${qi.table}.create`,
204
205
  // Non-RETURNING engines: INSERT, then re-fetch the new row by primary key
205
206
  // (provided value, else the driver's generated insert id).
206
- reselect: makeCreateReselect(qi, sql, params, args.data),
207
+ reselect: makeCreateReselect(qi, sql, params, args.data, projection),
207
208
  };
208
209
  }
209
210
  /**
@@ -212,7 +213,7 @@ function buildCreate(qi, args) {
212
213
  * dialect's result strategy is `'reselect'`, so the PostgreSQL/RETURNING path
213
214
  * pays nothing. Not yet wired to a real non-RETURNING engine.
214
215
  */
215
- function makeCreateReselect(qi, insertSql, insertParams, data) {
216
+ function makeCreateReselect(qi, insertSql, insertParams, data, projection) {
216
217
  if (qi.dialect.resultStrategy !== 'reselect')
217
218
  return undefined;
218
219
  return async (exec) => {
@@ -229,7 +230,7 @@ function makeCreateReselect(qi, insertSql, insertParams, data) {
229
230
  conds.push(`${qi.q(pk)} = ${qi.p(idx++)}`);
230
231
  }
231
232
  const where = conds.length > 0 ? ` WHERE ${conds.join(' AND ')}` : '';
232
- return exec(`SELECT ${writeReselectSelection(qi)} FROM ${qi.q(qi.table)}${where}`, selParams);
233
+ return exec(`SELECT ${writeReselectSelection(qi, projection)} FROM ${qi.q(qi.table)}${where}`, selParams);
233
234
  };
234
235
  }
235
236
  /**
@@ -388,7 +389,7 @@ function buildCreateMany(qi, args) {
388
389
  tag: `${qi.table}.createMany`,
389
390
  };
390
391
  }
391
- function buildUpdate(qi, args) {
392
+ function buildUpdate(qi, args, projection) {
392
393
  assertWritable(qi, 'update');
393
394
  qi.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
394
395
  // `updatedAt`-tagged columns are filled in before anything reads `data`, so
@@ -431,7 +432,7 @@ function buildUpdate(qi, args) {
431
432
  // version check that must still run.
432
433
  const hasSetData = Object.values(dataObj).some((v) => v !== undefined);
433
434
  if (!hasSetData && !lock) {
434
- const sel = buildReselectByWhere(qi, whereObj);
435
+ const sel = buildReselectByWhere(qi, whereObj, projection);
435
436
  return {
436
437
  sql: sel.sql,
437
438
  params: sel.params,
@@ -439,14 +440,16 @@ function buildUpdate(qi, args) {
439
440
  const row = result.rows[0];
440
441
  if (!row)
441
442
  throw new errors_js_1.NotFoundError({ table: qi.table, where: args.where, operation: 'update' });
442
- return parseWriteRow(qi, row);
443
+ return parseWriteRow(qi, row, projection);
443
444
  },
444
445
  tag: `${qi.table}.update`,
445
446
  };
446
447
  }
447
448
  const setFp = fingerprintSet(qi, dataObj);
448
449
  const whereFp = whereMod.fingerprintWhere(qi, whereObj);
449
- const ck = lock ? null : `u:${setFp}|${whereFp}${whereMod.globalFilterCacheSegment(qi)}`;
450
+ const ck = lock
451
+ ? null
452
+ : `u:${setFp}|${whereFp}${whereMod.globalFilterCacheSegment(qi)}${writeProjectionCacheSegment(projection)}`;
450
453
  const params = [];
451
454
  const buildSql = (freshParams) => {
452
455
  const setEntries = writeEntries(qi, dataObj);
@@ -467,7 +470,7 @@ function buildUpdate(qi, args) {
467
470
  // `OUTPUT INSERTED.*` between SET and WHERE) override buildUpdateStatement;
468
471
  // absent → the trailing-clause PG/SQLite/MySQL form (byte-identical).
469
472
  // `returning` excludes PII columns on tagged tables (else '*').
470
- const returning = writeReturningColumns(qi);
473
+ const returning = writeReturningColumns(qi, projection);
471
474
  return qi.dialect.buildUpdateStatement
472
475
  ? qi.dialect.buildUpdateStatement({ table: qi.q(qi.table), setClauses, whereSql, returning })
473
476
  : `UPDATE ${qi.q(qi.table)} SET ${setClauses.join(', ')}${whereSql}${qi.dialect.buildReturningClause(returning)}`;
@@ -512,7 +515,7 @@ function buildUpdate(qi, args) {
512
515
  operation: 'update',
513
516
  });
514
517
  }
515
- return parseWriteRow(qi, row);
518
+ return parseWriteRow(qi, row, projection);
516
519
  },
517
520
  tag: `${qi.table}.update`,
518
521
  preparedName,
@@ -532,13 +535,13 @@ function buildUpdate(qi, args) {
532
535
  expectedVersion: lock.expected,
533
536
  });
534
537
  }
535
- const sel = buildReselectByWhere(qi, whereObj);
538
+ const sel = buildReselectByWhere(qi, whereObj, projection);
536
539
  return exec(sel.sql, sel.params);
537
540
  }
538
541
  : undefined,
539
542
  };
540
543
  }
541
- function buildDelete(qi, args) {
544
+ function buildDelete(qi, args, projection) {
542
545
  assertWritable(qi, 'delete');
543
546
  qi.currentSkip = (0, types_js_1.resolveSkipGlobalFilters)(args.skipGlobalFilters);
544
547
  // Prisma compound-unique selector → the column conjunction (before the guard).
@@ -552,7 +555,7 @@ function buildDelete(qi, args) {
552
555
  (0, compound_unique_js_1.assertMutationWhereIdentifiesOneRow)(qi.tableMeta, qi.table, userWhere, 'delete');
553
556
  const whereObj = (whereMod.mergeGlobalFilter(qi, userWhere) ?? {});
554
557
  const whereFp = whereMod.fingerprintWhere(qi, whereObj);
555
- const ck = `d:${whereFp}${whereMod.globalFilterCacheSegment(qi)}`;
558
+ const ck = `d:${whereFp}${whereMod.globalFilterCacheSegment(qi)}${writeProjectionCacheSegment(projection)}`;
556
559
  const params = [];
557
560
  const buildSql = (freshParams) => {
558
561
  const clause = whereMod.buildWhereClause(qi, whereObj, freshParams);
@@ -560,7 +563,7 @@ function buildDelete(qi, args) {
560
563
  // SQL Server injects `OUTPUT DELETED.*` between `DELETE FROM <t>` and WHERE;
561
564
  // absent override → the trailing-clause PG/SQLite/MySQL form (byte-identical).
562
565
  // `returning` excludes PII columns on tagged tables (else '*').
563
- const returning = writeReturningColumns(qi);
566
+ const returning = writeReturningColumns(qi, projection);
564
567
  return qi.dialect.buildDeleteStatement
565
568
  ? qi.dialect.buildDeleteStatement({ table: qi.q(qi.table), whereSql, returning })
566
569
  : `DELETE FROM ${qi.q(qi.table)}${whereSql}${qi.dialect.buildReturningClause(returning)}`;
@@ -580,7 +583,7 @@ function buildDelete(qi, args) {
580
583
  operation: 'delete',
581
584
  });
582
585
  }
583
- return parseWriteRow(qi, row);
586
+ return parseWriteRow(qi, row, projection);
584
587
  },
585
588
  tag: `${qi.table}.delete`,
586
589
  preparedName: entry.name,
@@ -588,7 +591,7 @@ function buildDelete(qi, args) {
588
591
  // by the same where, then run the DELETE, returning the captured row.
589
592
  reselect: qi.dialect.resultStrategy === 'reselect'
590
593
  ? async (exec) => {
591
- const sel = buildReselectByWhere(qi, whereObj);
594
+ const sel = buildReselectByWhere(qi, whereObj, projection);
592
595
  const pre = await exec(sel.sql, sel.params);
593
596
  await exec(entry.sql, params, entry.name);
594
597
  return pre;
@@ -596,7 +599,7 @@ function buildDelete(qi, args) {
596
599
  : undefined,
597
600
  };
598
601
  }
599
- function buildUpsert(qi, args) {
602
+ function buildUpsert(qi, args, projection) {
600
603
  assertWritable(qi, 'upsert');
601
604
  assertNoGeneratedColumns(qi, args.create, 'upsert');
602
605
  assertNoGeneratedColumns(qi, args.update, 'upsert');
@@ -652,8 +655,23 @@ function buildUpsert(qi, args) {
652
655
  // unqualified column is ambiguous and PostgreSQL rejected EVERY upsert on a
653
656
  // globally filtered table at parse time (42702), insert path included.
654
657
  let updateWhere;
658
+ const upsertFilter = whereMod.resolveGlobalFilter(qi, qi.table);
659
+ if (upsertFilter && !qi.dialect.supportsUpsertUpdateWhere) {
660
+ // MySQL's `ON DUPLICATE KEY UPDATE` has no predicate slot and SQL Server's
661
+ // MERGE cannot take the builder's column references there, so on those
662
+ // engines the filter used to be DROPPED from the conflict update, silently.
663
+ // A tenant-scoped upsert whose key matched another tenant's row updated
664
+ // that row. Refused instead, when the filter would compile to anything:
665
+ // the global-filter contract is that it scopes every update, and an
666
+ // upsert that cannot honour it must not run as if it did.
667
+ if (whereMod.buildRenderedRefWhere(qi, qi.table, qi.tableMeta, qi.q(qi.table), upsertFilter, [])) {
668
+ throw new errors_js_1.UnsupportedFeatureError(`upsert on "${qi.table}", which has a global filter,`, qi.dialect.name, "This engine's upsert statement cannot carry the filter on its conflict update, so it could update a row " +
669
+ 'the filter hides. Use findUnique, then update or create, inside $transaction; or pass ' +
670
+ '`skipGlobalFilters: UNSAFE` if the upsert is meant to reach every row.');
671
+ }
672
+ }
655
673
  if (qi.dialect.supportsUpsertUpdateWhere) {
656
- const gf = whereMod.resolveGlobalFilter(qi, qi.table);
674
+ const gf = upsertFilter;
657
675
  if (gf) {
658
676
  // Compiled through a scope whose FROM-item reference is ALREADY RENDERED
659
677
  // (`"users"`), which is what the target table is inside `ON CONFLICT ...
@@ -678,7 +696,7 @@ function buildUpsert(qi, args) {
678
696
  conflictColumns,
679
697
  updateSetClauses: setClauses,
680
698
  updateWhere,
681
- returning: writeReturningColumns(qi),
699
+ returning: writeReturningColumns(qi, projection),
682
700
  });
683
701
  return {
684
702
  sql,
@@ -703,14 +721,14 @@ function buildUpsert(qi, args) {
703
721
  : `upsert on "${qi.table}" returned no row from RETURNING *; this should never happen.`,
704
722
  });
705
723
  }
706
- return parseWriteRow(qi, row);
724
+ return parseWriteRow(qi, row, projection);
707
725
  },
708
726
  tag: `${qi.table}.upsert`,
709
727
  // Non-RETURNING engines: run the upsert, then re-fetch by the where keys.
710
728
  reselect: qi.dialect.resultStrategy === 'reselect'
711
729
  ? async (exec) => {
712
730
  await exec(sql, params);
713
- const sel = buildReselectByWhere(qi, (whereMod.mergeGlobalFilter(qi, upsertWhere) ?? {}));
731
+ const sel = buildReselectByWhere(qi, (whereMod.mergeGlobalFilter(qi, upsertWhere) ?? {}), projection);
714
732
  return exec(sel.sql, sel.params);
715
733
  }
716
734
  : undefined,
@@ -859,6 +877,17 @@ function piiFields(_qi, meta) {
859
877
  }
860
878
  return out;
861
879
  }
880
+ /**
881
+ * The write-SQL cache segment for a projection. EMPTY for the default shape, so
882
+ * every existing key is byte-identical; a projected statement differs only in
883
+ * its RETURNING/OUTPUT text, and without this segment two calls with the same
884
+ * SET and WHERE but different `select`s would share one cached statement, i.e.
885
+ * the second caller would get the first caller's columns. NUL is the delimiter
886
+ * because it is the one byte a PostgreSQL identifier cannot contain.
887
+ */
888
+ function writeProjectionCacheSegment(projection) {
889
+ return projection ? `|rt=${projection.columns.join('\u0000')}` : '';
890
+ }
862
891
  /**
863
892
  * The `RETURNING` / `OUTPUT` selection for a write on this table. A table with
864
893
  * no PII column returns `'*'` (every column, byte-identical SQL to before);
@@ -925,7 +954,9 @@ function namedColumns(meta, data) {
925
954
  }
926
955
  return out;
927
956
  }
928
- function writeReturningColumns(qi) {
957
+ function writeReturningColumns(qi, projection) {
958
+ if (projection)
959
+ return projection.columns.map((col) => qi.q(col));
929
960
  // The PK exemption used to be re-stated here as `|| pk.has(col)`. It now
930
961
  // lives in `piiColumns` so every projection inherits it and none can drift.
931
962
  const piiCols = piiColumns(qi, qi.tableMeta);
@@ -938,8 +969,8 @@ function writeReturningColumns(qi) {
938
969
  * `'reselect'` result strategy re-fetches via a SELECT, not RETURNING).
939
970
  * `'*'` when there is no PII column; otherwise the comma-joined quoted list.
940
971
  */
941
- function writeReselectSelection(qi) {
942
- const cols = writeReturningColumns(qi);
972
+ function writeReselectSelection(qi, projection) {
973
+ const cols = writeReturningColumns(qi, projection);
943
974
  return cols === '*' ? '*' : cols.join(', ');
944
975
  }
945
976
  /**
@@ -950,10 +981,17 @@ function writeReselectSelection(qi) {
950
981
  * no-op. Untagged tables incur only one `for` over a zero-length field list,
951
982
  * so behavior is unchanged.
952
983
  */
953
- function parseWriteRow(qi, row) {
984
+ function parseWriteRow(qi, row, projection) {
954
985
  const parsed = qi.parseRow(row, qi.table);
955
- for (const field of piiFields(qi, qi.tableMeta)) {
956
- delete parsed[field];
986
+ // A column the caller named in `select` is the opt-in (see WriteProjection),
987
+ // so the strip spares exactly the projected columns and nothing else.
988
+ const pii = piiColumns(qi, qi.tableMeta);
989
+ if (pii.size === 0)
990
+ return parsed;
991
+ const requested = projection ? new Set(projection.columns) : undefined;
992
+ for (const col of qi.tableMeta.columns) {
993
+ if (pii.has(col.name) && !requested?.has(col.name))
994
+ delete parsed[col.field];
957
995
  }
958
996
  return parsed;
959
997
  }
@@ -24,6 +24,18 @@
24
24
  * (Neon HTTP, Vercel Postgres over fetch) cannot hold such a connection, so
25
25
  * `$listen` will surface a clear error rather than hang. `$notify` works
26
26
  * everywhere, it's a single round-trip `SELECT pg_notify(...)`.
27
+ *
28
+ * Connection loss:
29
+ *
30
+ * A subscription's connection is held indefinitely with no query in flight,
31
+ * so a database restart, failover or `pg_terminate_backend` reaches it only
32
+ * as an `'error'` event. That event used to have no listener, which exits the
33
+ * process. It is now guarded (connection-guard.ts), and the subscription
34
+ * RECONNECTS by default: the dead connection is destroyed, a fresh one is
35
+ * checked out with exponential backoff, and `LISTEN` is re-issued. Postgres
36
+ * does not queue notifications for a listener that is not connected, so
37
+ * anything NOTIFYed during the gap is gone for good; `onReconnect` is the
38
+ * caller's cue to resynchronise from the source of truth.
27
39
  */
28
40
  import type { PgCompatPool } from './client.js';
29
41
  /**
@@ -36,6 +48,34 @@ import type { PgCompatPool } from './client.js';
36
48
  export declare function validateChannel(channel: string): void;
37
49
  /** Handler invoked with the raw NOTIFY payload string (empty string if none). */
38
50
  export type NotificationHandler = (payload: string) => void;
51
+ /** Backoff for re-establishing a subscription whose connection was lost. */
52
+ export interface ListenReconnectOptions {
53
+ /** Delay before the first reconnect attempt. Default 100 ms. */
54
+ initialDelayMs?: number;
55
+ /** Ceiling for the doubling delay between attempts. Default 30,000 ms. */
56
+ maxDelayMs?: number;
57
+ }
58
+ /** Options for `$listen`. */
59
+ export interface ListenOptions {
60
+ /**
61
+ * Re-establish the subscription after its connection is lost (a restart,
62
+ * failover, compute suspend, `pg_terminate_backend`). Default `true`. With
63
+ * `false` the subscription ends at the first loss, after `onError`.
64
+ */
65
+ reconnect?: boolean | ListenReconnectOptions;
66
+ /**
67
+ * Called when the connection is lost and after each failed reconnect
68
+ * attempt. Defaults to one `console.error` line per event. The subscription
69
+ * keeps retrying unless `reconnect` is `false`; call `unsubscribe()` to stop.
70
+ */
71
+ onError?: (err: Error) => void;
72
+ /**
73
+ * Called once `LISTEN` is active again on a fresh connection. Notifications
74
+ * sent while the subscription was disconnected were NOT delivered and never
75
+ * will be, so this is where to resynchronise from the source of truth.
76
+ */
77
+ onReconnect?: () => void;
78
+ }
39
79
  /**
40
80
  * A live LISTEN subscription. Call `unsubscribe()` to UNLISTEN, detach the
41
81
  * handler, and release the dedicated connection back to the pool.
@@ -46,6 +86,7 @@ export interface Subscription {
46
86
  /**
47
87
  * Stop listening: runs `UNLISTEN "chan"`, removes the notification listener,
48
88
  * and releases the dedicated connection. Idempotent, safe to call twice.
89
+ * Also cancels a pending reconnect.
49
90
  */
50
91
  unsubscribe(): Promise<void>;
51
92
  }
@@ -59,12 +100,15 @@ export interface ActiveSubscription extends Subscription {
59
100
  }
60
101
  /**
61
102
  * Acquire a dedicated connection, run `LISTEN "channel"`, and wire the handler.
103
+ * A failure here is thrown to the `$listen` caller; only a connection lost
104
+ * AFTER the subscription is established is retried.
62
105
  *
63
106
  * @param pool the pg-compatible pool to check a long-lived client out of
64
107
  * @param channel channel name, MUST already be validated by the caller
65
108
  * @param quotedChannel the channel run through quoteIdent (interpolated into SQL)
66
109
  * @param handler called with each notification's payload
67
- * @param onClosed invoked when the subscription releases, so the client can
110
+ * @param onClosed invoked when the subscription ends, so the client can
68
111
  * drop it from its active-subscription registry
112
+ * @param options reconnect policy and loss/reconnect callbacks
69
113
  */
70
- export declare function createSubscription(pool: PgCompatPool, channel: string, quotedChannel: string, handler: NotificationHandler, onClosed: (sub: ActiveSubscription) => void): Promise<ActiveSubscription>;
114
+ export declare function createSubscription(pool: PgCompatPool, channel: string, quotedChannel: string, handler: NotificationHandler, onClosed: (sub: ActiveSubscription) => void, options?: ListenOptions): Promise<ActiveSubscription>;