turbine-orm 0.50.0 → 0.51.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 (186) hide show
  1. package/README.md +66 -66
  2. package/dist/adapters/cockroachdb.d.ts +5 -5
  3. package/dist/adapters/cockroachdb.js +10 -10
  4. package/dist/adapters/index.d.ts +5 -5
  5. package/dist/adapters/index.js +7 -7
  6. package/dist/adapters/yugabytedb.d.ts +7 -7
  7. package/dist/adapters/yugabytedb.js +10 -10
  8. package/dist/cjs/adapters/cockroachdb.d.ts +5 -5
  9. package/dist/cjs/adapters/cockroachdb.js +10 -10
  10. package/dist/cjs/adapters/index.d.ts +5 -5
  11. package/dist/cjs/adapters/index.js +7 -7
  12. package/dist/cjs/adapters/yugabytedb.d.ts +7 -7
  13. package/dist/cjs/adapters/yugabytedb.js +10 -10
  14. package/dist/cjs/cli/config.d.ts +13 -2
  15. package/dist/cjs/cli/config.js +3 -2
  16. package/dist/cjs/cli/destructive.d.ts +1 -1
  17. package/dist/cjs/cli/destructive.js +1 -1
  18. package/dist/cjs/cli/index.d.ts +10 -10
  19. package/dist/cjs/cli/index.js +49 -45
  20. package/dist/cjs/cli/loader.d.ts +7 -7
  21. package/dist/cjs/cli/loader.js +9 -9
  22. package/dist/cjs/cli/mcp.js +4 -4
  23. package/dist/cjs/cli/migrate.d.ts +5 -5
  24. package/dist/cjs/cli/migrate.js +11 -11
  25. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  26. package/dist/cjs/cli/ui.d.ts +2 -2
  27. package/dist/cjs/cli/ui.js +2 -2
  28. package/dist/cjs/client.d.ts +49 -38
  29. package/dist/cjs/client.js +57 -56
  30. package/dist/cjs/dialect.d.ts +62 -18
  31. package/dist/cjs/dialect.js +40 -2
  32. package/dist/cjs/errors.d.ts +5 -5
  33. package/dist/cjs/errors.js +11 -11
  34. package/dist/cjs/generate.d.ts +6 -6
  35. package/dist/cjs/generate.js +31 -29
  36. package/dist/cjs/index-advisor.d.ts +5 -5
  37. package/dist/cjs/index-advisor.js +0 -0
  38. package/dist/cjs/index.d.ts +1 -1
  39. package/dist/cjs/index.js +7 -7
  40. package/dist/cjs/introspect.d.ts +35 -9
  41. package/dist/cjs/introspect.js +83 -32
  42. package/dist/cjs/mssql.d.ts +11 -11
  43. package/dist/cjs/mssql.js +64 -29
  44. package/dist/cjs/mysql.d.ts +8 -8
  45. package/dist/cjs/mysql.js +61 -23
  46. package/dist/cjs/nested-write.d.ts +21 -2
  47. package/dist/cjs/nested-write.js +51 -14
  48. package/dist/cjs/optional-peer-import.cjs +7 -7
  49. package/dist/cjs/optional-peer-import.d.cts +7 -7
  50. package/dist/cjs/pipeline-submittable.d.ts +2 -2
  51. package/dist/cjs/pipeline-submittable.js +6 -6
  52. package/dist/cjs/pipeline.d.ts +1 -1
  53. package/dist/cjs/pipeline.js +4 -4
  54. package/dist/cjs/powdb-introspect.d.ts +1 -1
  55. package/dist/cjs/powdb-introspect.js +1 -1
  56. package/dist/cjs/powdb.d.ts +28 -28
  57. package/dist/cjs/powdb.js +66 -66
  58. package/dist/cjs/powql.d.ts +27 -27
  59. package/dist/cjs/powql.js +73 -52
  60. package/dist/cjs/query/aggregates.d.ts +1 -1
  61. package/dist/cjs/query/aggregates.js +5 -5
  62. package/dist/cjs/query/batched-loader.d.ts +11 -11
  63. package/dist/cjs/query/batched-loader.js +24 -24
  64. package/dist/cjs/query/builder.d.ts +39 -21
  65. package/dist/cjs/query/builder.js +99 -57
  66. package/dist/cjs/query/compound-unique.d.ts +1 -1
  67. package/dist/cjs/query/compound-unique.js +0 -0
  68. package/dist/cjs/query/deferred.d.ts +12 -6
  69. package/dist/cjs/query/deferred.js +1 -1
  70. package/dist/cjs/query/filters.d.ts +31 -11
  71. package/dist/cjs/query/filters.js +67 -14
  72. package/dist/cjs/query/index.d.ts +1 -1
  73. package/dist/cjs/query/index.js +1 -1
  74. package/dist/cjs/query/relations.d.ts +9 -9
  75. package/dist/cjs/query/relations.js +164 -57
  76. package/dist/cjs/query/types.d.ts +86 -35
  77. package/dist/cjs/query/types.js +1 -1
  78. package/dist/cjs/query/utils.d.ts +27 -10
  79. package/dist/cjs/query/utils.js +86 -14
  80. package/dist/cjs/query/where.d.ts +47 -28
  81. package/dist/cjs/query/where.js +130 -31
  82. package/dist/cjs/query/writes.d.ts +24 -5
  83. package/dist/cjs/query/writes.js +102 -13
  84. package/dist/cjs/realtime.d.ts +7 -7
  85. package/dist/cjs/realtime.js +9 -9
  86. package/dist/cjs/schema-builder.d.ts +18 -7
  87. package/dist/cjs/schema-builder.js +17 -10
  88. package/dist/cjs/schema-metadata.d.ts +3 -3
  89. package/dist/cjs/schema-metadata.js +9 -9
  90. package/dist/cjs/schema-sql.d.ts +9 -9
  91. package/dist/cjs/schema-sql.js +20 -20
  92. package/dist/cjs/schema.d.ts +19 -9
  93. package/dist/cjs/schema.js +6 -6
  94. package/dist/cjs/serverless.d.ts +15 -15
  95. package/dist/cjs/serverless.js +16 -16
  96. package/dist/cjs/sqlite.d.ts +8 -8
  97. package/dist/cjs/sqlite.js +53 -22
  98. package/dist/cjs/typed-sql.d.ts +4 -4
  99. package/dist/cjs/typed-sql.js +5 -5
  100. package/dist/cli/config.d.ts +13 -2
  101. package/dist/cli/config.js +3 -2
  102. package/dist/cli/destructive.d.ts +1 -1
  103. package/dist/cli/destructive.js +1 -1
  104. package/dist/cli/index.d.ts +10 -10
  105. package/dist/cli/index.js +49 -45
  106. package/dist/cli/loader.d.ts +7 -7
  107. package/dist/cli/loader.js +9 -9
  108. package/dist/cli/mcp.js +4 -4
  109. package/dist/cli/migrate.d.ts +5 -5
  110. package/dist/cli/migrate.js +11 -11
  111. package/dist/cli/studio-ui.generated.js +1 -1
  112. package/dist/cli/ui.d.ts +2 -2
  113. package/dist/cli/ui.js +2 -2
  114. package/dist/client.d.ts +49 -38
  115. package/dist/client.js +57 -56
  116. package/dist/dialect.d.ts +62 -18
  117. package/dist/dialect.js +40 -2
  118. package/dist/errors.d.ts +5 -5
  119. package/dist/errors.js +11 -11
  120. package/dist/generate.d.ts +6 -6
  121. package/dist/generate.js +31 -29
  122. package/dist/index-advisor.d.ts +5 -5
  123. package/dist/index-advisor.js +0 -0
  124. package/dist/index.d.ts +1 -1
  125. package/dist/index.js +7 -7
  126. package/dist/introspect.d.ts +35 -9
  127. package/dist/introspect.js +82 -32
  128. package/dist/mssql.d.ts +11 -11
  129. package/dist/mssql.js +64 -29
  130. package/dist/mysql.d.ts +8 -8
  131. package/dist/mysql.js +61 -23
  132. package/dist/nested-write.d.ts +21 -2
  133. package/dist/nested-write.js +51 -14
  134. package/dist/optional-peer-import.cjs +7 -7
  135. package/dist/optional-peer-import.d.cts +7 -7
  136. package/dist/pipeline-submittable.d.ts +2 -2
  137. package/dist/pipeline-submittable.js +6 -6
  138. package/dist/pipeline.d.ts +1 -1
  139. package/dist/pipeline.js +4 -4
  140. package/dist/powdb-introspect.d.ts +1 -1
  141. package/dist/powdb-introspect.js +1 -1
  142. package/dist/powdb.d.ts +28 -28
  143. package/dist/powdb.js +66 -66
  144. package/dist/powql.d.ts +27 -27
  145. package/dist/powql.js +73 -52
  146. package/dist/query/aggregates.d.ts +1 -1
  147. package/dist/query/aggregates.js +5 -5
  148. package/dist/query/batched-loader.d.ts +11 -11
  149. package/dist/query/batched-loader.js +24 -24
  150. package/dist/query/builder.d.ts +39 -21
  151. package/dist/query/builder.js +100 -58
  152. package/dist/query/compound-unique.d.ts +1 -1
  153. package/dist/query/compound-unique.js +0 -0
  154. package/dist/query/deferred.d.ts +12 -6
  155. package/dist/query/deferred.js +1 -1
  156. package/dist/query/filters.d.ts +31 -11
  157. package/dist/query/filters.js +66 -13
  158. package/dist/query/index.d.ts +1 -1
  159. package/dist/query/index.js +1 -1
  160. package/dist/query/relations.d.ts +9 -9
  161. package/dist/query/relations.js +165 -58
  162. package/dist/query/types.d.ts +86 -35
  163. package/dist/query/types.js +1 -1
  164. package/dist/query/utils.d.ts +27 -10
  165. package/dist/query/utils.js +84 -14
  166. package/dist/query/where.d.ts +47 -28
  167. package/dist/query/where.js +129 -32
  168. package/dist/query/writes.d.ts +24 -5
  169. package/dist/query/writes.js +101 -13
  170. package/dist/realtime.d.ts +7 -7
  171. package/dist/realtime.js +9 -9
  172. package/dist/schema-builder.d.ts +18 -7
  173. package/dist/schema-builder.js +17 -10
  174. package/dist/schema-metadata.d.ts +3 -3
  175. package/dist/schema-metadata.js +9 -9
  176. package/dist/schema-sql.d.ts +9 -9
  177. package/dist/schema-sql.js +20 -20
  178. package/dist/schema.d.ts +19 -9
  179. package/dist/schema.js +6 -6
  180. package/dist/serverless.d.ts +15 -15
  181. package/dist/serverless.js +16 -16
  182. package/dist/sqlite.d.ts +8 -8
  183. package/dist/sqlite.js +53 -22
  184. package/dist/typed-sql.d.ts +4 -4
  185. package/dist/typed-sql.js +5 -5
  186. package/package.json +2 -2
@@ -57,6 +57,7 @@ exports.buildUpdateMany = buildUpdateMany;
57
57
  exports.buildDeleteMany = buildDeleteMany;
58
58
  exports.piiColumns = piiColumns;
59
59
  exports.piiFields = piiFields;
60
+ exports.applyUpdatedAtColumns = applyUpdatedAtColumns;
60
61
  exports.writeReturningColumns = writeReturningColumns;
61
62
  exports.writeReselectSelection = writeReselectSelection;
62
63
  exports.parseWriteRow = parseWriteRow;
@@ -77,10 +78,10 @@ const whereMod = __importStar(require("./where.js"));
77
78
  *
78
79
  * Two value shapes need rewriting, both a JS `Date` on a temporal column:
79
80
  *
80
- * 1. A time-of-day column (`time` / `timetz`) — the driver serializes a `Date`
81
+ * 1. A time-of-day column (`time` / `timetz`), the driver serializes a `Date`
81
82
  * as a full ISO timestamp with the process offset and Postgres answers
82
83
  * `22007 invalid input syntax for type time`. Rewritten on every engine.
83
- * 2. A zone-less `date` / `timestamp` column on PostgreSQL — the driver's
84
+ * 2. A zone-less `date` / `timestamp` column on PostgreSQL, the driver's
84
85
  * local-offset serialization stores the PROCESS's calendar fields, so in a
85
86
  * non-UTC process the stored value is wrong and (because the read path
86
87
  * interprets an offset-less value as UTC) does not round-trip. Rewritten to
@@ -115,7 +116,7 @@ function coerceWriteValue(qi, key, value) {
115
116
  * PostgreSQL only: MySQL's `TIMESTAMP`/`DATETIME` and SQL Server's
116
117
  * `datetime2` are converted or bound by their own drivers, and MySQL in
117
118
  * particular reads a zone-less literal in the SESSION time zone, so a UTC
118
- * literal would be misread there. `utcTimestamps: false` opts out — it is the
119
+ * literal would be misread there. `utcTimestamps: false` opts out, it is the
119
120
  * same switch that turns off the UTC READ parsing, so the two stay symmetric.
120
121
  */
121
122
  function utcDateTimeWrites(qi) {
@@ -214,7 +215,7 @@ function buildCreateMany(qi, args) {
214
215
  return keys.map((key) => coerceWriteValue(qi, key, record[key]));
215
216
  });
216
217
  // Use actual Postgres types for array casts in the default PostgreSQL dialect.
217
- // Enum columns cast to `"EnumName"[]` — the generic text[] fallback would
218
+ // Enum columns cast to `"EnumName"[]`, the generic text[] fallback would
218
219
  // type the UNNEST output as text, which Postgres refuses to coerce to the
219
220
  // enum ("column X is of type Y but expression is of type text").
220
221
  const typeCasts = columns.map((col) => {
@@ -222,6 +223,18 @@ function buildCreateMany(qi, args) {
222
223
  return enumType ? `${qi.q(enumType)}[]` : whereMod.getColumnArrayType(qi, col);
223
224
  });
224
225
  const quotedColumns = columns.map((c) => qi.q(c));
226
+ // ARRAY-TYPED COLUMNS force the row-at-a-time VALUES form.
227
+ //
228
+ // The default PostgreSQL shape is `SELECT * FROM UNNEST($1::text[], …)`,
229
+ // one bound array per COLUMN. That is a column-major transpose, and it
230
+ // cannot express an array-valued column: `unnest` flattens, so N rows each
231
+ // holding a `text[]` arrive as one flat `text[]` and PostgreSQL rejects the
232
+ // insert with 42804 ("column is of type text[] but expression is of type
233
+ // text"). Single-row `create` never hit it because it binds each value
234
+ // directly. Detected from the column's own declared type rather than from
235
+ // the shape of the first row's value, so a row whose array column happens to
236
+ // be null or absent still takes the correct form.
237
+ const hasArrayColumn = columns.some((col) => (qi.columnPgTypeMap.get(col) ?? '').startsWith('_'));
225
238
  const built = qi.dialect.buildBulkInsertStatement({
226
239
  table: qt,
227
240
  columns: quotedColumns,
@@ -229,6 +242,7 @@ function buildCreateMany(qi, args) {
229
242
  columnArrayTypes: typeCasts,
230
243
  skipDuplicates: args.skipDuplicates,
231
244
  returning: writeReturningColumns(qi),
245
+ requireRowValues: hasArrayColumn,
232
246
  });
233
247
  return {
234
248
  sql: built.sql,
@@ -240,20 +254,51 @@ function buildCreateMany(qi, args) {
240
254
  function buildUpdate(qi, args) {
241
255
  assertWritable(qi, 'update');
242
256
  qi.currentSkip = args.skipGlobalFilters;
243
- const dataObj = args.data;
257
+ // `updatedAt`-tagged columns are filled in before anything reads `data`, so
258
+ // the SET list, the fingerprint and the param collector all see one object.
259
+ const dataObj = applyUpdatedAtColumns(qi, args.data);
244
260
  assertNoGeneratedColumns(qi, dataObj, 'update');
245
261
  // Prisma compound-unique selector (e.g. `{ orgId_userId: { orgId, userId } }`)
246
262
  // → the column conjunction, before the empty-`where` guard so the expanded
247
263
  // members count as a real predicate.
248
264
  const userWhere = (0, compound_unique_js_1.expandCompoundUniqueWhere)(qi.tableMeta, args.where);
249
265
  const lock = args.optimisticLock;
250
- // The empty-`where` guard checks the USER predicate only — a global filter
266
+ // The empty-`where` guard checks the USER predicate only, a global filter
251
267
  // must never turn an unguarded mass update into an allowed one.
252
268
  const userHasPredicate = !whereMod.userPredicateIsEmpty(qi, userWhere) || !!lock;
253
269
  whereMod.assertMutationHasPredicate(qi, 'update', userHasPredicate ? ' WHERE x' : '', args.allowFullTableScan);
254
270
  // The SQL is built from the global-filter-merged where (soft-delete keeps an
255
271
  // update from touching already-deleted rows).
256
272
  const whereObj = (whereMod.mergeGlobalFilter(qi, userWhere) ?? {});
273
+ // An update with nothing to set is a NO-OP that returns the current row.
274
+ //
275
+ // It used to render `UPDATE t SET WHERE …` and fail with a raw PostgreSQL
276
+ // 42601 syntax error. That is easy to hit honestly: any handler that builds
277
+ // its payload from optional request fields produces `{}` on a request that
278
+ // supplied none of them, so a legitimate request turned into a 500. Prisma
279
+ // treats the same call as a no-op and returns the row, so a ported handler
280
+ // inherited a crash where the original returned 200.
281
+ //
282
+ // The row is re-selected through the same projection a real update would
283
+ // return (PII columns excluded on tagged tables), and a where matching no
284
+ // row still raises NotFoundError, so only the SQL differs, not the contract.
285
+ // `optimisticLock` is excluded: it always has a version column to SET and a
286
+ // version check that must still run.
287
+ const hasSetData = Object.values(dataObj).some((v) => v !== undefined);
288
+ if (!hasSetData && !lock) {
289
+ const sel = buildReselectByWhere(qi, whereObj);
290
+ return {
291
+ sql: sel.sql,
292
+ params: sel.params,
293
+ transform: (result) => {
294
+ const row = result.rows[0];
295
+ if (!row)
296
+ throw new errors_js_1.NotFoundError({ table: qi.table, where: args.where, operation: 'update' });
297
+ return parseWriteRow(qi, row);
298
+ },
299
+ tag: `${qi.table}.update`,
300
+ };
301
+ }
257
302
  const setFp = fingerprintSet(qi, dataObj);
258
303
  const whereFp = whereMod.fingerprintWhere(qi, whereObj);
259
304
  const ck = lock ? null : `u:${setFp}|${whereFp}${whereMod.globalFilterCacheSegment(qi)}`;
@@ -333,7 +378,7 @@ function buildUpdate(qi, args) {
333
378
  // Optimistic-lock conflict: the version-checked UPDATE matched no
334
379
  // row. The re-fetch below uses `where` WITHOUT the version
335
380
  // predicate, so it would return the stale row and silently mask
336
- // the conflict — detect it from affected-rows here instead, to
381
+ // the conflict, detect it from affected-rows here instead, to
337
382
  // match the OptimisticLockError thrown on RETURNING/OUTPUT engines.
338
383
  if (lock && (writeResult.rowCount ?? 0) === 0) {
339
384
  throw new errors_js_1.OptimisticLockError({
@@ -414,7 +459,7 @@ function buildUpsert(qi, args) {
414
459
  const createParams = createEntries.map(([k, v]) => coerceWriteValue(qi, k, v));
415
460
  // Enum columns get an explicit `::"EnumName"` cast (see enumTypeForColumn).
416
461
  const placeholders = createEntries.map(([k], i) => `${qi.p(i + 1)}${whereMod.enumCastSuffix(qi, qi.toColumn(k))}`);
417
- // The conflict target comes from `where` keys — must be unique/PK columns
462
+ // The conflict target comes from `where` keys, must be unique/PK columns
418
463
  const conflictKeys = Object.keys(upsertWhere).filter((k) => upsertWhere[k] !== undefined);
419
464
  const conflictColumns = conflictKeys.map((k) => qi.toSqlColumn(k));
420
465
  // Build the UPDATE SET part
@@ -475,10 +520,22 @@ function buildUpsert(qi, args) {
475
520
  function buildUpdateMany(qi, args) {
476
521
  assertWritable(qi, 'updateMany');
477
522
  qi.currentSkip = args.skipGlobalFilters;
478
- const dataObj = args.data;
523
+ const dataObj = applyUpdatedAtColumns(qi, args.data);
479
524
  assertNoGeneratedColumns(qi, dataObj, 'updateMany');
480
525
  whereMod.assertMutationHasPredicate(qi, 'updateMany', whereMod.userPredicateIsEmpty(qi, args.where) ? '' : ' WHERE x', args.allowFullTableScan);
481
526
  const whereObj = (whereMod.mergeGlobalFilter(qi, args.where) ?? {});
527
+ // Nothing to SET: a no-op, for the same reason as `update` above (that path
528
+ // has the full rationale). Reports `count: 0` because zero rows were
529
+ // modified, no statement is issued at all, so this is not the count of rows
530
+ // the predicate MATCHED.
531
+ if (!Object.values(dataObj).some((v) => v !== undefined)) {
532
+ return {
533
+ sql: `SELECT ${qi.p(1)} AS count`,
534
+ params: [0],
535
+ transform: () => ({ count: 0 }),
536
+ tag: `${qi.table}.updateMany`,
537
+ };
538
+ }
482
539
  const setFp = fingerprintSet(qi, dataObj);
483
540
  const whereFp = whereMod.fingerprintWhere(qi, whereObj);
484
541
  const ck = `um:${setFp}|${whereFp}${whereMod.globalFilterCacheSegment(qi)}`;
@@ -559,17 +616,49 @@ function piiFields(_qi, meta) {
559
616
  }
560
617
  /**
561
618
  * The `RETURNING` / `OUTPUT` selection for a write on this table. A table with
562
- * no PII column returns `'*'` (every column — byte-identical SQL to before);
619
+ * no PII column returns `'*'` (every column, byte-identical SQL to before);
563
620
  * a table WITH PII columns returns an explicit quoted list of every non-PII
564
621
  * column so the PII values never leave the database on a write. A PII-tagged
565
622
  * PRIMARY KEY column is kept in the projection regardless (the returned row
566
- * must stay addressable): tag sensitive data, not keys — a PII PK is
623
+ * must stay addressable): tag sensitive data, not keys, a PII PK is
567
624
  * documented out of scope for stripping. Writes accept no `select`/`includePii`
568
625
  * (unlike reads), so this is the whole write-return policy at the SQL level;
569
626
  * {@link parseWriteRow} remains as a defense-in-depth strip (a no-op once the
570
627
  * SQL already excludes the columns). Derived purely from static per-table
571
628
  * schema metadata, so the write SQL cache needs no extra key segment.
572
629
  */
630
+ /**
631
+ * Return `data` with every `updatedAt`-tagged column the caller did not name
632
+ * set to `now`, or the original object when the table has none.
633
+ *
634
+ * Prisma's `@updatedAt` has no turbine equivalent, so a migrated application
635
+ * had to remember the field on every single update, and the value is usually
636
+ * load-bearing in the response body, so forgetting it is a silent staleness
637
+ * bug rather than a crash. The tag is opt-in per column and never inferred
638
+ * from a column's name, so a schema that does not use it emits byte-identical
639
+ * SQL and an application already managing its own timestamp is untouched.
640
+ *
641
+ * The timestamp is generated CLIENT-side (like Prisma) rather than as a SQL
642
+ * `now()`, so it flows through the same temporal coercion as any other bound
643
+ * `Date` and lands in UTC on every engine.
644
+ *
645
+ * An explicit value always wins, including an explicit `null`: naming the
646
+ * column is a statement of intent.
647
+ */
648
+ function applyUpdatedAtColumns(qi, data) {
649
+ const tagged = qi.tableMeta.columns.filter((c) => c.updatedAt);
650
+ if (tagged.length === 0)
651
+ return data;
652
+ let out = null;
653
+ const now = new Date();
654
+ for (const col of tagged) {
655
+ if (Object.hasOwn(data, col.field) && data[col.field] !== undefined)
656
+ continue;
657
+ out ??= { ...data };
658
+ out[col.field] = now;
659
+ }
660
+ return out ?? data;
661
+ }
573
662
  function writeReturningColumns(qi) {
574
663
  const piiCols = piiColumns(qi, qi.tableMeta);
575
664
  if (piiCols.size === 0)
@@ -627,7 +716,7 @@ function assertNoGeneratedColumns(qi, data, operation) {
627
716
  const col = qi.tableMeta.columns.find((c) => c.field === key || c.name === key || c.name === (0, schema_js_1.camelToSnake)(key));
628
717
  if (col?.isGeneratedStored) {
629
718
  throw new errors_js_1.ValidationError(`[turbine] Cannot ${operation} "${qi.table}": column "${key}" is a GENERATED ALWAYS AS (…) STORED ` +
630
- 'column whose value the database computes — remove it from your data.');
719
+ 'column whose value the database computes, remove it from your data.');
631
720
  }
632
721
  }
633
722
  }
@@ -636,7 +725,7 @@ function assertNoGeneratedColumns(qi, data, operation) {
636
725
  *
637
726
  * Supports plain values and atomic operator objects ({ set, increment,
638
727
  * decrement, multiply, divide }). An operator object is detected ONLY when
639
- * it has EXACTLY one key that is one of the 5 operator keys — this avoids
728
+ * it has EXACTLY one key that is one of the 5 operator keys, this avoids
640
729
  * misinterpreting JSON column values like `{ set: 'x' }` as operators
641
730
  * (real operator objects always have exactly one key, and a plain JSON
642
731
  * payload that happens to have a single `set` key is extremely unusual).
@@ -1,11 +1,11 @@
1
1
  /**
2
- * turbine-orm — LISTEN/NOTIFY realtime pub/sub
2
+ * turbine-orm, LISTEN/NOTIFY realtime pub/sub
3
3
  *
4
4
  * Postgres LISTEN/NOTIFY is a first-class realtime primitive that neither
5
5
  * Prisma nor Drizzle expose ergonomically. This module backs the thin
6
6
  * `$listen` / `$notify` methods on TurbineClient.
7
7
  *
8
- * Design — **one dedicated connection per subscription**:
8
+ * Design, **one dedicated connection per subscription**:
9
9
  *
10
10
  * Each `$listen(channel, handler)` acquires its OWN long-lived client from
11
11
  * the pool, runs `LISTEN "chan"`, and keeps that connection checked out for
@@ -13,7 +13,7 @@
13
13
  * subscription owns its lifecycle, `unsubscribe()` cleanly UNLISTENs and
14
14
  * releases exactly one connection, and there is no shared multiplexing
15
15
  * state to reason about. The trade-off is one pool slot per active channel
16
- * — for the handful of channels a typical app listens on, that's a fine
16
+ * - for the handful of channels a typical app listens on, that's a fine
17
17
  * price for clarity. (A future optimization could multiplex many channels
18
18
  * over a single shared notification connection.)
19
19
  *
@@ -23,14 +23,14 @@
23
23
  * notification messages back to the client. Stateless HTTP drivers
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
- * everywhere — it's a single round-trip `SELECT pg_notify(...)`.
26
+ * everywhere, it's a single round-trip `SELECT pg_notify(...)`.
27
27
  */
28
28
  import type { PgCompatPool } from './client.js';
29
29
  /**
30
30
  * Validate a LISTEN/NOTIFY channel name. Throws ValidationError on anything
31
31
  * that isn't a plain, reasonable-length SQL identifier. This is enforced for
32
32
  * BOTH `$listen` (where the channel is interpolated) and `$notify` (where the
33
- * channel is a bound param) — defensive parity, and it catches user typos
33
+ * channel is a bound param), defensive parity, and it catches user typos
34
34
  * loudly.
35
35
  */
36
36
  export declare function validateChannel(channel: string): void;
@@ -45,7 +45,7 @@ export interface Subscription {
45
45
  readonly channel: string;
46
46
  /**
47
47
  * Stop listening: runs `UNLISTEN "chan"`, removes the notification listener,
48
- * and releases the dedicated connection. Idempotent — safe to call twice.
48
+ * and releases the dedicated connection. Idempotent, safe to call twice.
49
49
  */
50
50
  unsubscribe(): Promise<void>;
51
51
  }
@@ -61,7 +61,7 @@ export interface ActiveSubscription extends Subscription {
61
61
  * Acquire a dedicated connection, run `LISTEN "channel"`, and wire the handler.
62
62
  *
63
63
  * @param pool the pg-compatible pool to check a long-lived client out of
64
- * @param channel channel name — MUST already be validated by the caller
64
+ * @param channel channel name, MUST already be validated by the caller
65
65
  * @param quotedChannel the channel run through quoteIdent (interpolated into SQL)
66
66
  * @param handler called with each notification's payload
67
67
  * @param onClosed invoked when the subscription releases, so the client can
@@ -1,12 +1,12 @@
1
1
  "use strict";
2
2
  /**
3
- * turbine-orm — LISTEN/NOTIFY realtime pub/sub
3
+ * turbine-orm, LISTEN/NOTIFY realtime pub/sub
4
4
  *
5
5
  * Postgres LISTEN/NOTIFY is a first-class realtime primitive that neither
6
6
  * Prisma nor Drizzle expose ergonomically. This module backs the thin
7
7
  * `$listen` / `$notify` methods on TurbineClient.
8
8
  *
9
- * Design — **one dedicated connection per subscription**:
9
+ * Design, **one dedicated connection per subscription**:
10
10
  *
11
11
  * Each `$listen(channel, handler)` acquires its OWN long-lived client from
12
12
  * the pool, runs `LISTEN "chan"`, and keeps that connection checked out for
@@ -14,7 +14,7 @@
14
14
  * subscription owns its lifecycle, `unsubscribe()` cleanly UNLISTENs and
15
15
  * releases exactly one connection, and there is no shared multiplexing
16
16
  * state to reason about. The trade-off is one pool slot per active channel
17
- * — for the handful of channels a typical app listens on, that's a fine
17
+ * - for the handful of channels a typical app listens on, that's a fine
18
18
  * price for clarity. (A future optimization could multiplex many channels
19
19
  * over a single shared notification connection.)
20
20
  *
@@ -24,7 +24,7 @@
24
24
  * notification messages back to the client. Stateless HTTP drivers
25
25
  * (Neon HTTP, Vercel Postgres over fetch) cannot hold such a connection, so
26
26
  * `$listen` will surface a clear error rather than hang. `$notify` works
27
- * everywhere — it's a single round-trip `SELECT pg_notify(...)`.
27
+ * everywhere, it's a single round-trip `SELECT pg_notify(...)`.
28
28
  */
29
29
  Object.defineProperty(exports, "__esModule", { value: true });
30
30
  exports.validateChannel = validateChannel;
@@ -37,7 +37,7 @@ const errors_js_1 = require("./errors.js");
37
37
  * Strict Postgres identifier: a letter or underscore followed by letters,
38
38
  * digits, or underscores. Channel names CANNOT be parameterized in
39
39
  * LISTEN/UNLISTEN (`LISTEN $1` is a syntax error), so the channel is the one
40
- * place an identifier is interpolated into SQL — it MUST pass this regex AND
40
+ * place an identifier is interpolated into SQL, it MUST pass this regex AND
41
41
  * go through `quoteIdent` before reaching the SQL string.
42
42
  */
43
43
  const CHANNEL_REGEX = /^[A-Za-z_][A-Za-z0-9_]*$/;
@@ -47,7 +47,7 @@ const MAX_CHANNEL_LEN = 63;
47
47
  * Validate a LISTEN/NOTIFY channel name. Throws ValidationError on anything
48
48
  * that isn't a plain, reasonable-length SQL identifier. This is enforced for
49
49
  * BOTH `$listen` (where the channel is interpolated) and `$notify` (where the
50
- * channel is a bound param) — defensive parity, and it catches user typos
50
+ * channel is a bound param), defensive parity, and it catches user typos
51
51
  * loudly.
52
52
  */
53
53
  function validateChannel(channel) {
@@ -58,7 +58,7 @@ function validateChannel(channel) {
58
58
  throw new errors_js_1.ValidationError(`[turbine] $listen/$notify channel "${channel}" exceeds the ${MAX_CHANNEL_LEN}-character Postgres identifier limit`);
59
59
  }
60
60
  if (!CHANNEL_REGEX.test(channel)) {
61
- throw new errors_js_1.ValidationError(`[turbine] Invalid $listen/$notify channel "${channel}" — must match /^[A-Za-z_][A-Za-z0-9_]*$/ ` +
61
+ throw new errors_js_1.ValidationError(`[turbine] Invalid $listen/$notify channel "${channel}", must match /^[A-Za-z_][A-Za-z0-9_]*$/ ` +
62
62
  '(letters, digits, underscores; cannot start with a digit)');
63
63
  }
64
64
  }
@@ -66,7 +66,7 @@ function validateChannel(channel) {
66
66
  * Acquire a dedicated connection, run `LISTEN "channel"`, and wire the handler.
67
67
  *
68
68
  * @param pool the pg-compatible pool to check a long-lived client out of
69
- * @param channel channel name — MUST already be validated by the caller
69
+ * @param channel channel name, MUST already be validated by the caller
70
70
  * @param quotedChannel the channel run through quoteIdent (interpolated into SQL)
71
71
  * @param handler called with each notification's payload
72
72
  * @param onClosed invoked when the subscription releases, so the client can
@@ -81,7 +81,7 @@ async function createSubscription(pool, channel, quotedChannel, handler, onClose
81
81
  throw (0, errors_js_1.wrapPgError)(err);
82
82
  }
83
83
  // Verify the checked-out client can actually receive async notifications.
84
- // Stateless HTTP drivers return a client with no `.on` — LISTEN would hang
84
+ // Stateless HTTP drivers return a client with no `.on`, LISTEN would hang
85
85
  // forever waiting for messages that can never arrive, so fail loudly now and
86
86
  // give the connection straight back.
87
87
  if (typeof client.on !== 'function') {
@@ -1,8 +1,8 @@
1
1
  /**
2
- * turbine-orm — Schema Builder
2
+ * turbine-orm, Schema Builder
3
3
  *
4
4
  * TypeScript-first schema definition API. Define your database schema
5
- * as plain objects — no method chaining, no DSL. Fully type-checked,
5
+ * as plain objects, no method chaining, no DSL. Fully type-checked,
6
6
  * JSON-serializable, and easy to read.
7
7
  *
8
8
  * @example
@@ -56,9 +56,9 @@ export interface ColumnDef {
56
56
  references?: string | ReferenceDef;
57
57
  /** Max length for varchar columns */
58
58
  maxLength?: number;
59
- /** Enum type name — required when `type: 'enum'`. */
59
+ /** Enum type name, required when `type: 'enum'`. */
60
60
  enumName?: string;
61
- /** pgvector dimension count — required when `type: 'vector'`. */
61
+ /** pgvector dimension count, required when `type: 'vector'`. */
62
62
  dimensions?: number;
63
63
  /** When true, the column is an array of `type` (e.g. `text[]`). */
64
64
  array?: boolean;
@@ -73,6 +73,13 @@ export interface ColumnDef {
73
73
  * `includePii: true`) and redacted by Studio. Introspection never auto-tags PII.
74
74
  */
75
75
  pii?: boolean;
76
+ /**
77
+ * Set this column to the current time on every `update` that does not name
78
+ * it explicitly (Prisma's `@updatedAt`). Opt-in per column and never
79
+ * inferred from the column name: see
80
+ * {@link import('./schema.js').ColumnMetadata.updatedAt}.
81
+ */
82
+ updatedAt?: boolean;
76
83
  }
77
84
  /** Postgres-level column type (uppercase, as used in DDL) */
78
85
  export type ColumnType = 'SERIAL' | 'BIGSERIAL' | 'BIGINT' | 'INTEGER' | 'SMALLINT' | 'TEXT' | 'BOOLEAN' | 'TIMESTAMPTZ' | 'JSONB' | 'UUID' | 'REAL' | 'DOUBLE PRECISION' | 'NUMERIC' | 'BYTEA' | 'DATE' | 'VARCHAR' | 'ENUM' | 'VECTOR';
@@ -99,13 +106,15 @@ export interface ColumnConfig {
99
106
  check: string | null;
100
107
  /** Whether this column is tagged as PII (personally identifiable information). */
101
108
  pii: boolean;
109
+ /** Whether this column is auto-set to the current time on update. */
110
+ updatedAt: boolean;
102
111
  }
103
112
  /**
104
113
  * Explicit many-to-many relation declaration for the code-first schema.
105
114
  *
106
115
  * Auto-detecting m2m from a junction table is intentionally conservative (a
107
116
  * junction with payload columns is treated as a first-class entity, not a join
108
- * table — see `introspect.ts`). This declaration lets users opt in to an m2m
117
+ * table, see `introspect.ts`). This declaration lets users opt in to an m2m
109
118
  * relation explicitly, mirroring how Prisma/Drizzle require an explicit
110
119
  * `@relation` / `relation()` for join tables.
111
120
  *
@@ -196,7 +205,7 @@ export interface TableDef {
196
205
  /**
197
206
  * Optional composite primary key. When present, takes precedence over any
198
207
  * column-level `primaryKey: true` flags. Column names listed here are the
199
- * camelCase JS-facing field names — they will be converted to snake_case
208
+ * camelCase JS-facing field names, they will be converted to snake_case
200
209
  * when emitted as a `PRIMARY KEY (...)` table constraint.
201
210
  */
202
211
  primaryKey?: readonly string[];
@@ -311,6 +320,8 @@ export declare class ColumnBuilder {
311
320
  }): this;
312
321
  check(expression: string): this;
313
322
  pii(): this;
323
+ /** Auto-set this column to the current time on every update. Prisma's `@updatedAt`. */
324
+ updatedAt(): this;
314
325
  array(): this;
315
326
  build(): ColumnConfig;
316
327
  }
@@ -331,7 +342,7 @@ export declare function table(columns: Record<string, ColumnBuilder>): TableDef;
331
342
  *
332
343
  * This is the runtime bridge for the code-first m2m API: `defineSchema` only
333
344
  * produces DDL, so after `introspect()`ing the live database you call this to
334
- * attach the m2m relations you declared. It is PURELY ADDITIVE — existing
345
+ * attach the m2m relations you declared. It is PURELY ADDITIVE, existing
335
346
  * belongsTo/hasMany/hasOne relations are preserved, and a declared relation is
336
347
  * skipped (not overwritten) if its name already exists on the source table.
337
348
  *
@@ -1,9 +1,9 @@
1
1
  "use strict";
2
2
  /**
3
- * turbine-orm — Schema Builder
3
+ * turbine-orm, Schema Builder
4
4
  *
5
5
  * TypeScript-first schema definition API. Define your database schema
6
- * as plain objects — no method chaining, no DSL. Fully type-checked,
6
+ * as plain objects, no method chaining, no DSL. Fully type-checked,
7
7
  * JSON-serializable, and easy to read.
8
8
  *
9
9
  * @example
@@ -32,7 +32,7 @@ exports.applyManyToManyRelations = applyManyToManyRelations;
32
32
  /** Maps shorthand names to actual Postgres type strings */
33
33
  const TYPE_MAP = {
34
34
  // `serial` maps to SERIAL (int4). Its values fit in a JS `number` and pg
35
- // returns them as numbers — so the generated `number` type is accurate.
35
+ // returns them as numbers, so the generated `number` type is accurate.
36
36
  // For 64-bit auto-increment keys use `bigserial` (int8), noting that values
37
37
  // above Number.MAX_SAFE_INTEGER read back as string. (Changed in 0.24.0;
38
38
  // `serial` previously emitted BIGSERIAL.)
@@ -44,7 +44,7 @@ const TYPE_MAP = {
44
44
  text: 'TEXT',
45
45
  varchar: 'VARCHAR',
46
46
  boolean: 'BOOLEAN',
47
- // `timestamp` is an honest alias for TIMESTAMPTZ (timezone-aware) — Turbine
47
+ // `timestamp` is an honest alias for TIMESTAMPTZ (timezone-aware), Turbine
48
48
  // has always emitted TIMESTAMPTZ for it. `timestamptz` is the explicit spelling.
49
49
  timestamp: 'TIMESTAMPTZ',
50
50
  timestamptz: 'TIMESTAMPTZ',
@@ -58,7 +58,7 @@ const TYPE_MAP = {
58
58
  double: 'DOUBLE PRECISION',
59
59
  numeric: 'NUMERIC',
60
60
  bytea: 'BYTEA',
61
- // Sentinels — the real DDL type is derived from `enumName` / `dimensions`
61
+ // Sentinels, the real DDL type is derived from `enumName` / `dimensions`
62
62
  // in schema-sql.ts, never from these placeholders.
63
63
  enum: 'ENUM',
64
64
  vector: 'VECTOR',
@@ -102,6 +102,7 @@ function resolveColumn(def) {
102
102
  isArray: def.array ?? false,
103
103
  check: def.check ?? null,
104
104
  pii: def.pii ?? false,
105
+ updatedAt: def.updatedAt ?? false,
105
106
  };
106
107
  }
107
108
  /** Type guard: is this index declaration a doc-field expression index? */
@@ -195,7 +196,7 @@ function defineSchema(input, options) {
195
196
  }
196
197
  // Validate composite PK references real columns and clear column-level PKs
197
198
  // for those columns so we don't double-emit `PRIMARY KEY` clauses.
198
- // Composite PK members are implicitly NOT NULL — preserve that even
199
+ // Composite PK members are implicitly NOT NULL, preserve that even
199
200
  // when the user clears the column-level `primaryKey: true` flag.
200
201
  if (pk && pk.length > 0) {
201
202
  for (const colName of pk) {
@@ -204,7 +205,7 @@ function defineSchema(input, options) {
204
205
  `Known columns: ${Object.keys(columns).join(', ') || '(none)'}`);
205
206
  }
206
207
  // A composite PK at the table level supersedes any column-level
207
- // `primaryKey: true` flag — silently clear it so DDL emission
208
+ // `primaryKey: true` flag, silently clear it so DDL emission
208
209
  // produces a single, valid table-level PRIMARY KEY constraint.
209
210
  // Force NOT NULL since PK columns can never be nullable.
210
211
  const c = columns[colName];
@@ -235,7 +236,7 @@ function camelToSnakeLocal(s) {
235
236
  return s.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`);
236
237
  }
237
238
  // ---------------------------------------------------------------------------
238
- // Legacy compat — ColumnBuilder still works for existing code
239
+ // Legacy compat, ColumnBuilder still works for existing code
239
240
  // ---------------------------------------------------------------------------
240
241
  class ColumnBuilder {
241
242
  _config;
@@ -256,6 +257,7 @@ class ColumnBuilder {
256
257
  isArray: false,
257
258
  check: null,
258
259
  pii: false,
260
+ updatedAt: false,
259
261
  };
260
262
  }
261
263
  serial() {
@@ -366,6 +368,11 @@ class ColumnBuilder {
366
368
  this._config.pii = true;
367
369
  return this;
368
370
  }
371
+ /** Auto-set this column to the current time on every update. Prisma's `@updatedAt`. */
372
+ updatedAt() {
373
+ this._config.updatedAt = true;
374
+ return this;
375
+ }
369
376
  array() {
370
377
  this._config.isArray = true;
371
378
  return this;
@@ -429,7 +436,7 @@ function table(columns) {
429
436
  *
430
437
  * This is the runtime bridge for the code-first m2m API: `defineSchema` only
431
438
  * produces DDL, so after `introspect()`ing the live database you call this to
432
- * attach the m2m relations you declared. It is PURELY ADDITIVE — existing
439
+ * attach the m2m relations you declared. It is PURELY ADDITIVE, existing
433
440
  * belongsTo/hasMany/hasOne relations are preserved, and a declared relation is
434
441
  * skipped (not overwritten) if its name already exists on the source table.
435
442
  *
@@ -468,7 +475,7 @@ function applyManyToManyRelations(meta, def) {
468
475
  const sourceTable = tableDef.name;
469
476
  const sourceMeta = tables[sourceTable];
470
477
  if (!sourceMeta)
471
- continue; // table not present in introspected metadata — skip
478
+ continue; // table not present in introspected metadata, skip
472
479
  const relations = { ...sourceMeta.relations };
473
480
  for (const m of tableDef.manyToMany) {
474
481
  // Additive-only: never clobber an existing relation name.
@@ -1,9 +1,9 @@
1
1
  /**
2
- * turbine-orm — defineSchema() → SchemaMetadata bridge
2
+ * turbine-orm, defineSchema() → SchemaMetadata bridge
3
3
  *
4
4
  * Converts a code-first {@link SchemaDef} (the output of `defineSchema()`)
5
5
  * into the runtime {@link SchemaMetadata} shape that the query builder,
6
- * `TurbineClient`, and the non-SQL engines consume — without touching a
6
+ * `TurbineClient`, and the non-SQL engines consume, without touching a
7
7
  * live database.
8
8
  *
9
9
  * Why this exists: the historical converter path (`introspect()` +
@@ -49,7 +49,7 @@ import { type SchemaDef } from './schema-builder.js';
49
49
  /**
50
50
  * Convert a code-first {@link SchemaDef} into runtime {@link SchemaMetadata}.
51
51
  *
52
- * Pure function — no database connection, no side effects, input untouched.
52
+ * Pure function, no database connection, no side effects, input untouched.
53
53
  * The output is shaped identically to the `SCHEMA` constant `turbine generate`
54
54
  * emits from introspection, so it can be handed to any consumer that expects
55
55
  * introspected metadata: `new TurbineClient(config, metadata)`,
@@ -1,10 +1,10 @@
1
1
  "use strict";
2
2
  /**
3
- * turbine-orm — defineSchema() → SchemaMetadata bridge
3
+ * turbine-orm, defineSchema() → SchemaMetadata bridge
4
4
  *
5
5
  * Converts a code-first {@link SchemaDef} (the output of `defineSchema()`)
6
6
  * into the runtime {@link SchemaMetadata} shape that the query builder,
7
- * `TurbineClient`, and the non-SQL engines consume — without touching a
7
+ * `TurbineClient`, and the non-SQL engines consume, without touching a
8
8
  * live database.
9
9
  *
10
10
  * Why this exists: the historical converter path (`introspect()` +
@@ -80,13 +80,13 @@ function udtName(config) {
80
80
  return 'vector';
81
81
  return DDL_TO_UDT[config.type];
82
82
  }
83
- /** Server-generated (sequence-backed) types — pg's `nextval(...)` default. */
83
+ /** Server-generated (sequence-backed) types, pg's `nextval(...)` default. */
84
84
  function isSerialType(type) {
85
85
  return type === 'SERIAL' || type === 'BIGSERIAL';
86
86
  }
87
87
  /**
88
88
  * Resolve the raw column part of a `references: 'table.column'` target to a
89
- * snake_case column name — accepting either the camelCase field name or the
89
+ * snake_case column name, accepting either the camelCase field name or the
90
90
  * snake_case DDL name, mirroring how schema-sql.ts accepts both table forms.
91
91
  */
92
92
  function resolveColumnName(raw, target) {
@@ -166,7 +166,7 @@ function mapIndexes(tableDef, declared) {
166
166
  /**
167
167
  * Convert a code-first {@link SchemaDef} into runtime {@link SchemaMetadata}.
168
168
  *
169
- * Pure function — no database connection, no side effects, input untouched.
169
+ * Pure function, no database connection, no side effects, input untouched.
170
170
  * The output is shaped identically to the `SCHEMA` constant `turbine generate`
171
171
  * emits from introspection, so it can be handed to any consumer that expects
172
172
  * introspected metadata: `new TurbineClient(config, metadata)`,
@@ -200,7 +200,7 @@ function mapIndexes(tableDef, declared) {
200
200
  function schemaDefToMetadata(def) {
201
201
  // ----- Pass 1: resolve every table's snake_case names for FK lookups -----
202
202
  // Lookup accepts the accessor key (camelCase), the DDL name (snake_case),
203
- // and the explicit `accessor` field — same tolerance as schema-sql.ts.
203
+ // and the explicit `accessor` field, same tolerance as schema-sql.ts.
204
204
  const lookup = new Map();
205
205
  for (const [key, tableDef] of Object.entries(def.tables)) {
206
206
  const fieldToColumn = new Map();
@@ -227,7 +227,7 @@ function schemaDefToMetadata(def) {
227
227
  if (parts.length !== 2)
228
228
  continue;
229
229
  const target = lookup.get(parts[0]);
230
- // Reference to a table outside this SchemaDef — skip, exactly like
230
+ // Reference to a table outside this SchemaDef, skip, exactly like
231
231
  // introspection skips FKs whose target is excluded from the table set.
232
232
  if (!target)
233
233
  continue;
@@ -249,14 +249,14 @@ function schemaDefToMetadata(def) {
249
249
  // catalog: legacy-first naming, per-column disambiguation for several FKs to
250
250
  // the same target, and collision resolution against scalar column fields
251
251
  // (json/jsonb `unknown`-typed shadows keep the historical name). The old
252
- // local reimplementation had NO collision guard — `posts.user` (text) +
252
+ // local reimplementation had NO collision guard, `posts.user` (text) +
253
253
  // `userId references users.id` produced a relation `user` that shadowed the
254
254
  // scalar, and two FKs deriving the same name silently clobbered each other
255
255
  // (N-4).
256
256
  //
257
257
  // Constraint names are synthesized in pg's default `<table>_<column>_fkey`
258
258
  // form; they only feed the referential-action lookup and composite-FK
259
- // naming (never hit here — `references:` is single-column by design).
259
+ // naming (never hit here, `references:` is single-column by design).
260
260
  const fkEntries = [];
261
261
  const fkActions = new Map();
262
262
  for (const fk of foreignKeys) {