turbine-orm 0.49.0 → 0.50.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 (157) hide show
  1. package/README.md +122 -39
  2. package/dist/cjs/adapters/cockroachdb.d.ts +39 -0
  3. package/dist/cjs/adapters/index.d.ts +110 -0
  4. package/dist/cjs/adapters/yugabytedb.d.ts +51 -0
  5. package/dist/cjs/cli/config.d.ts +181 -0
  6. package/dist/cjs/cli/config.js +32 -6
  7. package/dist/cjs/cli/destructive.d.ts +38 -0
  8. package/dist/cjs/cli/index.d.ts +359 -0
  9. package/dist/cjs/cli/index.js +228 -56
  10. package/dist/cjs/cli/loader.d.ts +61 -0
  11. package/dist/cjs/cli/mcp.d.ts +42 -0
  12. package/dist/cjs/cli/migrate.d.ts +356 -0
  13. package/dist/cjs/cli/migrate.js +131 -40
  14. package/dist/cjs/cli/observe-ui.d.ts +1 -0
  15. package/dist/cjs/cli/observe-ui.js +14 -5
  16. package/dist/cjs/cli/observe.d.ts +25 -0
  17. package/dist/cjs/cli/observe.js +49 -12
  18. package/dist/cjs/cli/pii-tags.d.ts +53 -0
  19. package/dist/cjs/cli/prisma-report.d.ts +33 -0
  20. package/dist/cjs/cli/prisma-report.js +73 -0
  21. package/dist/cjs/cli/prisma-resolve.d.ts +106 -0
  22. package/dist/cjs/cli/prisma-resolve.js +1 -0
  23. package/dist/cjs/cli/prisma-schema.d.ts +176 -0
  24. package/dist/cjs/cli/prisma-schema.js +82 -4
  25. package/dist/cjs/cli/rate-limit.d.ts +32 -0
  26. package/dist/cjs/cli/rate-limit.js +45 -0
  27. package/dist/cjs/cli/studio-demo.d.ts +43 -0
  28. package/dist/cjs/cli/studio-ui.generated.d.ts +1 -0
  29. package/dist/cjs/cli/studio.d.ts +207 -0
  30. package/dist/cjs/cli/studio.js +136 -71
  31. package/dist/cjs/cli/ui.d.ts +73 -0
  32. package/dist/cjs/cli/ui.js +51 -9
  33. package/dist/cjs/client.d.ts +837 -0
  34. package/dist/cjs/client.js +3 -0
  35. package/dist/cjs/dialect.d.ts +516 -0
  36. package/dist/cjs/dialect.js +37 -12
  37. package/dist/cjs/errors.d.ts +370 -0
  38. package/dist/cjs/generate.d.ts +137 -0
  39. package/dist/cjs/generate.js +39 -6
  40. package/dist/cjs/index-advisor.d.ts +153 -0
  41. package/dist/cjs/index-stats.d.ts +384 -0
  42. package/dist/cjs/index.d.ts +55 -0
  43. package/dist/cjs/index.js +7 -2
  44. package/dist/cjs/introspect.d.ts +269 -0
  45. package/dist/cjs/mssql.d.ts +232 -0
  46. package/dist/cjs/mssql.js +6 -0
  47. package/dist/cjs/mysql.d.ts +173 -0
  48. package/dist/cjs/mysql.js +16 -0
  49. package/dist/cjs/nested-write.d.ts +96 -0
  50. package/dist/cjs/nested-write.js +414 -24
  51. package/dist/cjs/observe.d.ts +115 -0
  52. package/dist/cjs/optional-peer-import.d.cts +72 -0
  53. package/dist/cjs/pipeline-submittable.d.ts +93 -0
  54. package/dist/cjs/pipeline.d.ts +71 -0
  55. package/dist/cjs/powdb-introspect.d.ts +84 -0
  56. package/dist/cjs/powdb.d.ts +931 -0
  57. package/dist/cjs/powdb.js +106 -21
  58. package/dist/cjs/powql.d.ts +592 -0
  59. package/dist/cjs/powql.js +42 -6
  60. package/dist/cjs/prisma-compat.d.ts +283 -0
  61. package/dist/cjs/prisma-compat.js +167 -9
  62. package/dist/cjs/query/aggregates.d.ts +92 -0
  63. package/dist/cjs/query/aggregates.js +7 -3
  64. package/dist/cjs/query/batched-loader.d.ts +193 -0
  65. package/dist/cjs/query/builder.d.ts +849 -0
  66. package/dist/cjs/query/builder.js +571 -65
  67. package/dist/cjs/query/compound-unique.d.ts +51 -0
  68. package/dist/cjs/query/deferred.d.ts +223 -0
  69. package/dist/cjs/query/filters.d.ts +201 -0
  70. package/dist/cjs/query/index.d.ts +14 -0
  71. package/dist/cjs/query/index.js +6 -1
  72. package/dist/cjs/query/relations.d.ts +609 -0
  73. package/dist/cjs/query/relations.js +693 -46
  74. package/dist/cjs/query/types.d.ts +1300 -0
  75. package/dist/cjs/query/utils.d.ts +209 -0
  76. package/dist/cjs/query/utils.js +208 -1
  77. package/dist/cjs/query/warn-registry.d.ts +68 -0
  78. package/dist/cjs/query/warn-registry.js +9 -0
  79. package/dist/cjs/query/where-compile.d.ts +139 -0
  80. package/dist/cjs/query/where.d.ts +548 -0
  81. package/dist/cjs/query/where.js +58 -22
  82. package/dist/cjs/query/writes.d.ts +172 -0
  83. package/dist/cjs/query/writes.js +105 -12
  84. package/dist/cjs/realtime.d.ts +70 -0
  85. package/dist/cjs/schema-builder.d.ts +354 -0
  86. package/dist/cjs/schema-metadata.d.ts +83 -0
  87. package/dist/cjs/schema-sql.d.ts +217 -0
  88. package/dist/cjs/schema-sql.js +23 -5
  89. package/dist/cjs/schema.d.ts +356 -0
  90. package/dist/cjs/schema.js +125 -0
  91. package/dist/cjs/seed.d.ts +15 -0
  92. package/dist/cjs/serverless.d.ts +142 -0
  93. package/dist/cjs/sqlite.d.ts +143 -0
  94. package/dist/cjs/sqlite.js +4 -0
  95. package/dist/cjs/typed-sql.d.ts +102 -0
  96. package/dist/cli/config.d.ts +18 -4
  97. package/dist/cli/config.js +31 -6
  98. package/dist/cli/index.d.ts +123 -0
  99. package/dist/cli/index.js +223 -58
  100. package/dist/cli/migrate.d.ts +59 -10
  101. package/dist/cli/migrate.js +128 -41
  102. package/dist/cli/observe-ui.d.ts +1 -1
  103. package/dist/cli/observe-ui.js +14 -5
  104. package/dist/cli/observe.d.ts +7 -1
  105. package/dist/cli/observe.js +48 -12
  106. package/dist/cli/prisma-report.d.ts +14 -0
  107. package/dist/cli/prisma-report.js +72 -0
  108. package/dist/cli/prisma-resolve.d.ts +6 -0
  109. package/dist/cli/prisma-resolve.js +1 -0
  110. package/dist/cli/prisma-schema.d.ts +62 -2
  111. package/dist/cli/prisma-schema.js +81 -4
  112. package/dist/cli/rate-limit.d.ts +32 -0
  113. package/dist/cli/rate-limit.js +40 -0
  114. package/dist/cli/studio.d.ts +5 -5
  115. package/dist/cli/studio.js +135 -70
  116. package/dist/cli/ui.d.ts +1 -1
  117. package/dist/cli/ui.js +51 -9
  118. package/dist/client.d.ts +40 -0
  119. package/dist/client.js +3 -0
  120. package/dist/dialect.d.ts +17 -1
  121. package/dist/dialect.js +37 -12
  122. package/dist/generate.js +40 -7
  123. package/dist/index.d.ts +1 -1
  124. package/dist/index.js +1 -1
  125. package/dist/mssql.js +6 -0
  126. package/dist/mysql.js +16 -0
  127. package/dist/nested-write.d.ts +2 -0
  128. package/dist/nested-write.js +415 -25
  129. package/dist/powdb.d.ts +4 -2
  130. package/dist/powdb.js +106 -21
  131. package/dist/powql.d.ts +5 -0
  132. package/dist/powql.js +42 -6
  133. package/dist/prisma-compat.d.ts +2 -0
  134. package/dist/prisma-compat.js +166 -8
  135. package/dist/query/aggregates.js +7 -3
  136. package/dist/query/builder.d.ts +292 -21
  137. package/dist/query/builder.js +570 -64
  138. package/dist/query/deferred.d.ts +39 -0
  139. package/dist/query/index.d.ts +1 -1
  140. package/dist/query/index.js +1 -1
  141. package/dist/query/relations.d.ts +173 -5
  142. package/dist/query/relations.js +688 -47
  143. package/dist/query/types.d.ts +123 -39
  144. package/dist/query/utils.d.ts +116 -0
  145. package/dist/query/utils.js +198 -0
  146. package/dist/query/warn-registry.d.ts +9 -0
  147. package/dist/query/warn-registry.js +9 -0
  148. package/dist/query/where.d.ts +38 -1
  149. package/dist/query/where.js +58 -23
  150. package/dist/query/writes.d.ts +42 -1
  151. package/dist/query/writes.js +104 -13
  152. package/dist/schema-sql.d.ts +14 -0
  153. package/dist/schema-sql.js +23 -5
  154. package/dist/schema.d.ts +38 -0
  155. package/dist/schema.js +123 -0
  156. package/dist/sqlite.js +4 -0
  157. package/package.json +77 -28
package/dist/powdb.js CHANGED
@@ -141,8 +141,11 @@ export class PowdbFloatParam {
141
141
  */
142
142
  export class PowdbJsonParam {
143
143
  value;
144
- constructor(value) {
144
+ column;
145
+ /** `column` is diagnostic only: it names the target column when serialization fails. */
146
+ constructor(value, column) {
145
147
  this.value = value;
148
+ this.column = column;
146
149
  }
147
150
  }
148
151
  /** Minimum PowDB server version the networked transport requires. */
@@ -597,9 +600,10 @@ export function powqlSchemaDDL(schema, opts = {}) {
597
600
  // Declared indexes: PowDB doc-field expression indexes (docPath) and plain
598
601
  // single-column indexes. A doc-field index MUST be parenthesized (the engine
599
602
  // rejects a bare JSON path); string path segments emit lexer-exact via the
600
- // shared `encodePowqlString`, integer array indexes emit bare. A json
601
- // document column reference stays dotted-bare (`.col`), which bypasses
602
- // keyword lookup on every engine version exactly like a filter path.
603
+ // shared `encodePowqlString`, integer array indexes emit bare. The json
604
+ // document column goes through `quotePowqlIdent` exactly like the plain
605
+ // index branch below, so a keyword or non-bare identifier is backtick-quoted
606
+ // instead of being spliced raw into the statement.
603
607
  for (const idx of meta.indexes) {
604
608
  const kind = idx.unique ? 'unique' : 'index';
605
609
  if (idx.docPath) {
@@ -610,7 +614,7 @@ export function powqlSchemaDDL(schema, opts = {}) {
610
614
  throw new ValidationError(`[turbine] Doc-field index "${idx.name}" on ${meta.name} has no target json column.`);
611
615
  }
612
616
  const segs = idx.docPath.map((s) => (typeof s === 'number' ? `->${s}` : `->${encodePowqlString(s)}`)).join('');
613
- stmts.push(`alter ${quotePowqlIdent(meta.name)} add ${kind} (.${column}${segs})`);
617
+ stmts.push(`alter ${quotePowqlIdent(meta.name)} add ${kind} (.${quotePowqlIdent(column)}${segs})`);
614
618
  }
615
619
  else {
616
620
  // Plain column index. PowDB has no composite index (`add index` takes a
@@ -718,6 +722,36 @@ async function listPowdbLinks(exec) {
718
722
  function powdbCell(v) {
719
723
  return v === null || v === undefined ? '' : String(v);
720
724
  }
725
+ /**
726
+ * Serialize a json-column value to the canonical JSON text both transports
727
+ * send. `JSON.stringify` can throw (circular structure, a BigInt cell, a
728
+ * throwing `toJSON`) and can return `undefined` (a value that serializes to
729
+ * nothing, e.g. a `toJSON` returning undefined); both surface as a typed E003
730
+ * naming the column instead of a raw TypeError or an undefined param.
731
+ */
732
+ function powdbJsonText(param, position) {
733
+ const where = `for column "${param.column ?? '(unknown)'}"${position ? ` (${position})` : ''}`;
734
+ let json;
735
+ try {
736
+ json = JSON.stringify(param.value);
737
+ }
738
+ catch (err) {
739
+ throw new ValidationError(`[turbine] The json value ${where} cannot be serialized for PowDB: ${err.message}`);
740
+ }
741
+ if (json === undefined) {
742
+ throw new ValidationError(`[turbine] The json value ${where} serializes to nothing (JSON.stringify returned undefined); ` +
743
+ 'write `null` explicitly instead.');
744
+ }
745
+ return json;
746
+ }
747
+ /** Convert a Date to PowDB epoch micros, refusing an invalid Date (getTime() is NaN). */
748
+ function powdbDateMicros(value, position) {
749
+ const ms = value.getTime();
750
+ if (!Number.isFinite(ms)) {
751
+ throw new ValidationError(`[turbine] Invalid Date${position ? ` (${position})` : ''} cannot be encoded as a PowDB timestamp.`);
752
+ }
753
+ return BigInt(ms) * 1000n;
754
+ }
721
755
  /** Coerce a JS value into a PowDB positional param (the write side). */
722
756
  function toPowdbParam(value, col) {
723
757
  if (value instanceof PowdbFloatParam)
@@ -725,11 +759,11 @@ function toPowdbParam(value, col) {
725
759
  // json document: serialize to canonical JSON text and bind as a str param,
726
760
  // the engine validates it as JSON and stores the canonical binary form.
727
761
  if (value instanceof PowdbJsonParam)
728
- return JSON.stringify(value.value);
762
+ return powdbJsonText(value);
729
763
  if (value === undefined || value === null)
730
764
  return null;
731
765
  if (value instanceof Date)
732
- return BigInt(value.getTime()) * 1000n; // ms → micros (int column)
766
+ return powdbDateMicros(value); // ms → micros (int column)
733
767
  if (col && isDateColumn(col) && typeof value === 'number')
734
768
  return BigInt(value) * 1000n;
735
769
  if (typeof value === 'boolean' ||
@@ -1585,36 +1619,74 @@ function normalizeEmbeddedResult(r) {
1585
1619
  * quotes, backslashes, `$N`, `"); drop … --`, raw CR, and emoji all round-trip
1586
1620
  * as data and cannot break out of the literal or inject a second statement.
1587
1621
  */
1588
- export function encodePowqlLiteral(value) {
1622
+ export function encodePowqlLiteral(value, position) {
1623
+ const at = position ? ` (${position})` : '';
1589
1624
  if (value instanceof PowdbFloatParam) {
1590
1625
  const n = value.value;
1591
1626
  if (!Number.isFinite(n))
1592
- throw new ValidationError(`[turbine] Non-finite float cannot be encoded for PowDB.`);
1627
+ throw new ValidationError(`[turbine] Non-finite float cannot be encoded for PowDB${at}.`);
1593
1628
  // Force a float-form literal so an integer-valued float column stays a float.
1594
- return Number.isInteger(n) ? `${n}.0` : String(n);
1629
+ const text = powqlNumberText(n);
1630
+ return text.includes('.') ? text : `${text}.0`;
1595
1631
  }
1596
1632
  // json document: emit the canonical JSON text as a PowQL string literal (the
1597
1633
  // embedded engine validates and stores it as a json document).
1598
1634
  if (value instanceof PowdbJsonParam)
1599
- return encodePowqlString(JSON.stringify(value.value));
1635
+ return encodePowqlString(powdbJsonText(value, position), position);
1600
1636
  if (value === undefined || value === null)
1601
1637
  return 'null';
1602
1638
  if (value instanceof Date)
1603
- return `${BigInt(value.getTime()) * 1000n}`; // epoch micros (int column)
1639
+ return `${powdbDateMicros(value, position)}`; // epoch micros (int column)
1604
1640
  if (typeof value === 'boolean')
1605
1641
  return value ? 'true' : 'false';
1606
1642
  if (typeof value === 'bigint')
1607
1643
  return value.toString();
1608
1644
  if (typeof value === 'number') {
1609
1645
  if (!Number.isFinite(value))
1610
- throw new ValidationError(`[turbine] Non-finite number cannot be encoded for PowDB.`);
1611
- // `String(n)` renders an integer as an int literal (`42`) and a fractional
1612
- // number as a float literal (`4.2`) PowQL distinguishes them by the dot.
1613
- return String(value);
1646
+ throw new ValidationError(`[turbine] Non-finite number cannot be encoded for PowDB${at}.`);
1647
+ // Renders an integer as an int literal (`42`) and a fractional number as a
1648
+ // float literal (`4.2`); PowQL distinguishes them by the dot.
1649
+ return powqlNumberText(value);
1614
1650
  }
1615
1651
  if (typeof value === 'string')
1616
- return encodePowqlString(value);
1617
- throw new ValidationError(`[turbine] Value of type ${typeof value} cannot be encoded as a PowDB literal.`);
1652
+ return encodePowqlString(value, position);
1653
+ throw new ValidationError(`[turbine] Value of type ${typeof value} cannot be encoded as a PowDB literal${at}.`);
1654
+ }
1655
+ /**
1656
+ * Render a finite JS number as PowQL numeric text.
1657
+ *
1658
+ * `String(n)` is used verbatim for every ordinary magnitude (an integer becomes
1659
+ * `42`, a fractional number `4.2`), but it switches to exponential notation at
1660
+ * `1e21` and below `1e-6` (`1e+21`, `1.5e-7`), which the PowQL lexer does not
1661
+ * accept as a number token (and which the float path would further mangle into
1662
+ * `1e+21.0`). Those two ranges are expanded into plain decimal digits so any
1663
+ * finite double round-trips to a literal the engine can parse.
1664
+ */
1665
+ function powqlNumberText(n) {
1666
+ const s = String(n);
1667
+ if (!s.includes('e') && !s.includes('E'))
1668
+ return s;
1669
+ // Integer-valued doubles (|n| >= 1e21) expand exactly through BigInt.
1670
+ if (Number.isInteger(n))
1671
+ return BigInt(n).toString();
1672
+ const m = /^(-?)(\d+)(?:\.(\d+))?[eE]([+-]?\d+)$/.exec(s);
1673
+ if (!m) {
1674
+ throw new ValidationError(`[turbine] Number ${s} cannot be rendered as a PowDB literal.`);
1675
+ }
1676
+ const sign = m[1] ?? '';
1677
+ const intPart = m[2] ?? '';
1678
+ const fracPart = m[3] ?? '';
1679
+ const exp = Number(m[4]);
1680
+ const digits = intPart + fracPart;
1681
+ const pointPos = intPart.length + exp;
1682
+ let body;
1683
+ if (pointPos <= 0)
1684
+ body = `0.${'0'.repeat(-pointPos)}${digits}`;
1685
+ else if (pointPos >= digits.length)
1686
+ body = digits + '0'.repeat(pointPos - digits.length);
1687
+ else
1688
+ body = `${digits.slice(0, pointPos)}.${digits.slice(pointPos)}`;
1689
+ return sign + body;
1618
1690
  }
1619
1691
  /**
1620
1692
  * The newest PowDB engine LINE (major.minor) whose lexer escape handling
@@ -1646,8 +1718,21 @@ export function encodePowqlLiteral(value) {
1646
1718
  * already covers every 0.19.x patch — no bump is needed for 0.19.1.
1647
1719
  */
1648
1720
  export const POWQL_LEXER_TESTED_CEILING = '0.19';
1649
- /** Escape a string into a PowQL `"…"` literal, matching the engine lexer's escape rules. */
1650
- function encodePowqlString(s) {
1721
+ /**
1722
+ * Escape a string into a PowQL `"…"` literal, matching the engine lexer's
1723
+ * escape rules.
1724
+ *
1725
+ * A raw NUL (U+0000) is REFUSED rather than emitted: the lexer defines no `\0`
1726
+ * escape (its escape set is exactly `\"`, `\\`, `\n`, `\t`, see
1727
+ * {@link POWQL_LEXER_TESTED_CEILING}), so a NUL would travel into the query
1728
+ * text as a raw byte and any layer that treats that text as a C string would
1729
+ * silently truncate the statement there.
1730
+ */
1731
+ function encodePowqlString(s, position) {
1732
+ if (s.includes('\0')) {
1733
+ throw new ValidationError(`[turbine] String value${position ? ` (${position})` : ''} contains a NUL byte (U+0000), which PowQL string ` +
1734
+ 'literals cannot represent. Strip it before writing.');
1735
+ }
1651
1736
  let out = '"';
1652
1737
  for (const ch of s) {
1653
1738
  if (ch === '\\')
@@ -1677,7 +1762,7 @@ export function materializePowql(powql, params) {
1677
1762
  if (idx < 0 || idx >= params.length) {
1678
1763
  throw new ValidationError(`[turbine] PowQL placeholder $${n} has no bound parameter (have ${params.length}).`);
1679
1764
  }
1680
- return encodePowqlLiteral(params[idx]);
1765
+ return encodePowqlLiteral(params[idx], `parameter $${n}`);
1681
1766
  });
1682
1767
  }
1683
1768
  /**
package/dist/powql.d.ts CHANGED
@@ -556,6 +556,11 @@ export declare class PowqlInterface<T extends object = Record<string, unknown>>
556
556
  * with the projection); a per-field aggregate re-emits its inner expression
557
557
  * (from `aggInner` when the field is a requested aggregate, so a JSON-path
558
558
  * aggregate reuses its bound placeholders, else `.field` for a plain column).
559
+ *
560
+ * Every token that reaches the PowQL text is builder-controlled: the field is
561
+ * validated by `ref()`, the aggregate function comes from
562
+ * {@link POWQL_HAVING_AGG_FNS} (unknown keys throw E003), the operator from a
563
+ * fixed map, and every compared value is bound as a `$N` param.
559
564
  */
560
565
  private buildHaving;
561
566
  /**
package/dist/powql.js CHANGED
@@ -107,6 +107,21 @@ const OPERATOR_KEYS = new Set([
107
107
  'endsWith',
108
108
  'mode',
109
109
  ]);
110
+ /**
111
+ * Aggregate keys accepted inside a per-field `having` filter, mapped to the
112
+ * PowQL function each one emits. The set is FIXED: any other key is rejected by
113
+ * {@link PowqlInterface.buildHaving} rather than interpolated into the query
114
+ * text (the SQL builder holds the same contract, see `aggFnByKey` in
115
+ * query/aggregates.ts). A Map is used deliberately so an inherited object key
116
+ * (`constructor`, `toString`, `__proto__`) can never resolve to a function name.
117
+ */
118
+ const POWQL_HAVING_AGG_FNS = new Map([
119
+ ['_sum', 'sum'],
120
+ ['_avg', 'avg'],
121
+ ['_min', 'min'],
122
+ ['_max', 'max'],
123
+ ['_count', 'count'],
124
+ ]);
110
125
  /** Filters that have no PowDB representation and must throw E017. */
111
126
  function rejectUnsupportedFilter(value, field) {
112
127
  if ('distance' in value || 'metric' in value) {
@@ -206,7 +221,7 @@ export class PowqlInterface {
206
221
  // json document column: a JS object/array is serialized to canonical JSON
207
222
  // text and stored as a json document (a JS string passes through raw, same
208
223
  // contract as pg jsonb; `null` stays `null`).
209
- tagged = new PowdbJsonParam(value);
224
+ tagged = new PowdbJsonParam(value, col.name);
210
225
  }
211
226
  params.push(tagged);
212
227
  return `$${params.length}`;
@@ -2460,6 +2475,11 @@ export class PowqlInterface {
2460
2475
  * with the projection); a per-field aggregate re-emits its inner expression
2461
2476
  * (from `aggInner` when the field is a requested aggregate, so a JSON-path
2462
2477
  * aggregate reuses its bound placeholders, else `.field` for a plain column).
2478
+ *
2479
+ * Every token that reaches the PowQL text is builder-controlled: the field is
2480
+ * validated by `ref()`, the aggregate function comes from
2481
+ * {@link POWQL_HAVING_AGG_FNS} (unknown keys throw E003), the operator from a
2482
+ * fixed map, and every compared value is bound as a `$N` param.
2463
2483
  */
2464
2484
  buildHaving(having, params, aggInner) {
2465
2485
  if (!having)
@@ -2469,10 +2489,19 @@ export class PowqlInterface {
2469
2489
  if (typeof filter === 'number')
2470
2490
  return `${expr} = ${this.param(filter, params)}`;
2471
2491
  const f = filter;
2472
- const ops = { equals: '=', gt: '>', gte: '>=', lt: '<', lte: '<=', not: '!=' };
2492
+ // Map (not an object literal) so an inherited key such as `constructor`
2493
+ // cannot resolve to a truthy value and reach the emitted PowQL.
2494
+ const ops = new Map([
2495
+ ['equals', '='],
2496
+ ['gt', '>'],
2497
+ ['gte', '>='],
2498
+ ['lt', '<'],
2499
+ ['lte', '<='],
2500
+ ['not', '!='],
2501
+ ]);
2473
2502
  return Object.entries(f)
2474
- .filter(([k]) => ops[k])
2475
- .map(([k, v]) => `${expr} ${ops[k]} ${this.param(v, params)}`)
2503
+ .filter(([k]) => ops.has(k))
2504
+ .map(([k, v]) => `${expr} ${ops.get(k)} ${this.param(v, params)}`)
2476
2505
  .join(' and ');
2477
2506
  };
2478
2507
  for (const [key, spec] of Object.entries(having)) {
@@ -2482,11 +2511,18 @@ export class PowqlInterface {
2482
2511
  conds.push(cmp('count(*)', spec));
2483
2512
  }
2484
2513
  else {
2485
- for (const [fn, filter] of Object.entries(spec)) {
2514
+ for (const [aggKey, filter] of Object.entries(spec)) {
2486
2515
  if (filter == null)
2487
2516
  continue;
2517
+ // The function token is emitted verbatim into the PowQL text, so it
2518
+ // must come from the fixed allowlist, never from the caller's key.
2519
+ const fn = POWQL_HAVING_AGG_FNS.get(aggKey);
2520
+ if (!fn) {
2521
+ throw new ValidationError(`[turbine] Unknown aggregate "${aggKey}" in having for field "${key}" on table "${this.table}". ` +
2522
+ `Supported: ${[...POWQL_HAVING_AGG_FNS.keys()].join(', ')}.`);
2523
+ }
2488
2524
  const inner = aggInner.get(key) ?? this.ref(key);
2489
- conds.push(cmp(`${fn.slice(1)}(${inner})`, filter));
2525
+ conds.push(cmp(`${fn}(${inner})`, filter));
2490
2526
  }
2491
2527
  }
2492
2528
  }
@@ -260,6 +260,8 @@ export type PrismaCompatClient<S extends Record<string, PrismaModelTypes> = Reco
260
260
  } & {
261
261
  [K in keyof S as Uncapitalize<K & string>]: PrismaModelDelegate<S[K]>;
262
262
  } & PrismaCompatClientBase<S>;
263
+ /** @internal Names on a compat client that are NOT model delegates. */
264
+ export declare const CLIENT_RESERVED_KEYS: ReadonlySet<string>;
263
265
  /**
264
266
  * Create a PrismaClient-surface adapter over a {@link TurbineClient}, driven by a
265
267
  * {@link PrismaCompatMap} (the `prisma-map.ts` that `turbine
@@ -82,6 +82,7 @@
82
82
  * ```
83
83
  */
84
84
  import { TurbineError, TurbineErrorCode, UnsupportedFeatureError, ValidationError, wrapPgError } from './errors.js';
85
+ import { shouldWarnOnce } from './query/warn-registry.js';
85
86
  // ---------------------------------------------------------------------------
86
87
  // Prisma.sql-style raw fragments (local, minimal, never imports @prisma/client)
87
88
  // ---------------------------------------------------------------------------
@@ -399,7 +400,7 @@ function modelName(ctx, mm) {
399
400
  * `where` / `orderBy` are translated first so the cursor step (which may inject
400
401
  * a keyset predicate) operates in the translated turbine-field space.
401
402
  */
402
- function translateReadArgs(ctx, mm, prismaArgs) {
403
+ function translateReadArgs(ctx, mm, prismaArgs, kind) {
403
404
  const t = {};
404
405
  if (prismaArgs.where !== undefined)
405
406
  t.where = translateWhere(ctx, mm, prismaArgs.where);
@@ -424,9 +425,85 @@ function translateReadArgs(ctx, mm, prismaArgs) {
424
425
  t.includePii = prismaArgs.includePii;
425
426
  if (ctx.options.stablePkOrder)
426
427
  t.stableRelationOrder = true;
428
+ // ORDER IS LOAD-BEARING: translateCursor MUST run BEFORE applyImplicitPkOrder.
429
+ // The cursor translation reads `t.orderBy` to decide the seek direction and to
430
+ // validate that a bare inclusive cursor names the sort key; it must see the
431
+ // USER's ordering, never an ordering this layer invented. Swap these two lines
432
+ // and a bare inclusive cursor on a non-PK field silently compiles against the
433
+ // injected primary-key order instead of throwing. See the guard test
434
+ // "cursor decisions are made against the user's own orderBy".
427
435
  translateCursor(ctx, mm, prismaArgs, t);
436
+ applyImplicitPkOrder(ctx, mm, t, kind);
428
437
  return t;
429
438
  }
439
+ /**
440
+ * Prisma appends an implicit `ORDER BY <primary key> ASC` to a paginated read;
441
+ * Turbine emits a bare `LIMIT`. An unordered `LIMIT` is not stable in Postgres:
442
+ * once the heap changes underneath it (autovacuum, a heap rewrite, a plan flip)
443
+ * successive pages can repeat a row or skip one entirely, and the unordered
444
+ * plan is also the slower one (a seq scan instead of a primary-key index scan).
445
+ *
446
+ * So a top-level compat read whose page is not already ordered gets the model's
447
+ * primary key ascending, matching Prisma. An explicit `orderBy` always wins, a
448
+ * composite primary key orders on every column in declaration order, and a model
449
+ * with no primary key is left exactly as it was (nothing is invented).
450
+ *
451
+ * ## Which reads this covers
452
+ *
453
+ * - `findMany` only when it paginates (`take` / `skip`): without a limit there
454
+ * is no page to be unstable.
455
+ * - `findFirst` / `findFirstOrThrow` ALWAYS: core compiles both to a bare
456
+ * `LIMIT 1`, which is the single most common nondeterministic read shape in
457
+ * Prisma-shaped code ("give me any one row" quietly becoming "give me
458
+ * whichever row the heap hands back today").
459
+ * - `findUnique` / `findUniqueOrThrow` NEVER: they are addressed by a unique
460
+ * key, so at most one row matches and an ordering is pure overhead.
461
+ * - Nested relation includes (`include: { posts: { take: 3 } }`) are NOT
462
+ * touched here, deliberately. Relation ordering is a core concern (core owns
463
+ * the target table's metadata and the subquery shape), and core already
464
+ * exposes it as `stableRelationOrder`, which this layer forwards from the
465
+ * {@link PrismaCompatOptions.stablePkOrder} option. Re-walking the `with` tree
466
+ * here would duplicate that mechanism and fight it inside the subquery.
467
+ *
468
+ * ## Mirrored exclusions (keep in sync with core `isUnorderedPage`)
469
+ *
470
+ * Core's `QueryInterface.isUnorderedPage` (query/builder.ts) excludes both
471
+ * `distinct` and `cursor`. This layer mirrors them one at a time rather than
472
+ * copying the predicate, because the two layers do NOT have the same default:
473
+ *
474
+ * - `distinct`: EXCLUDED, same as core, and this one is a hard requirement.
475
+ * Adding an `orderBy` flips core's `DISTINCT ON` out of its plain branch into
476
+ * the two-level derived-table rewrite whose OUTER `ORDER BY` can only see the
477
+ * projected columns, so `distinct` + `select` + `take` would fail at runtime
478
+ * with `column "id" does not exist`. Injecting also lifts the `LIMIT` outside
479
+ * the `DISTINCT ON`, so the inner scan stops being limited. `distinct` reads
480
+ * keep the exact SQL they emitted before this ordering existed.
481
+ * - `cursor`: NOT excluded here, unlike core. Core is a general-purpose layer
482
+ * whose cursor callers chose their own ordering; this layer's contract is
483
+ * Prisma parity, and Prisma pairs cursor pagination with the same implicit
484
+ * primary-key order. The injection is safe because it runs strictly AFTER
485
+ * {@link translateCursor}: the cursor comparison direction was already
486
+ * resolved from the user's own `orderBy` (ascending by default, matching an
487
+ * ascending primary key), and the bare-inclusive-cursor branch has already
488
+ * set or demanded an `orderBy`, so only the exclusive `cursor` + `skip`
489
+ * branch reaches here. It gains an `ORDER BY <pk> ASC` and nothing else.
490
+ */
491
+ function applyImplicitPkOrder(ctx, mm, t, kind) {
492
+ if (kind === 'unique')
493
+ return;
494
+ if (t.orderBy !== undefined)
495
+ return;
496
+ if (t.distinct !== undefined)
497
+ return;
498
+ if (kind === 'many' && t.limit === undefined && t.offset === undefined)
499
+ return;
500
+ const meta = ctx.schema.tables[mm.table];
501
+ const pk = meta?.primaryKey ?? [];
502
+ if (pk.length === 0)
503
+ return;
504
+ const fields = pk.map((col) => meta?.reverseColumnMap?.[col] ?? col);
505
+ t.orderBy = fields.length === 1 ? { [fields[0]]: 'asc' } : fields.map((f) => ({ [f]: 'asc' }));
506
+ }
430
507
  /** turbine field names of the model's single-column primary key, if any. */
431
508
  function singleColumnPkField(ctx, mm) {
432
509
  const pk = ctx.schema.tables[mm.table]?.primaryKey;
@@ -1039,11 +1116,11 @@ function makeDelegate(ctx, mm, getQI, runInTx) {
1039
1116
  return args;
1040
1117
  };
1041
1118
  return {
1042
- findMany: (args = {}) => defer(() => translateReadArgs(ctx, mm, args), (qi, t) => qi.findMany(t).then((r) => reshapeRows(ctx, mm, r)), { build: (qi, t) => qi.buildFindMany(t), reshape: (raw) => reshapeRows(ctx, mm, raw) }),
1043
- findFirst: (args = {}) => defer(() => translateReadArgs(ctx, mm, args), (qi, t) => qi.findFirst(t).then((r) => reshapeRowOrNull(ctx, mm, r)), { build: (qi, t) => qi.buildFindFirst(t), reshape: (raw) => reshapeRowOrNull(ctx, mm, raw) }),
1044
- findUnique: (args) => defer(() => translateReadArgs(ctx, mm, requireWhere(args, 'findUnique')), (qi, t) => qi.findUnique(t).then((r) => reshapeRowOrNull(ctx, mm, r)), { build: (qi, t) => qi.buildFindUnique(t), reshape: (raw) => reshapeRowOrNull(ctx, mm, raw) }),
1045
- findFirstOrThrow: (args = {}) => defer(() => translateReadArgs(ctx, mm, args), (qi, t) => qi.findFirstOrThrow(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildFindFirstOrThrow(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
1046
- findUniqueOrThrow: (args) => defer(() => translateReadArgs(ctx, mm, requireWhere(args, 'findUniqueOrThrow')), (qi, t) => qi.findUniqueOrThrow(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildFindUniqueOrThrow(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
1119
+ findMany: (args = {}) => defer(() => translateReadArgs(ctx, mm, args, 'many'), (qi, t) => qi.findMany(t).then((r) => reshapeRows(ctx, mm, r)), { build: (qi, t) => qi.buildFindMany(t), reshape: (raw) => reshapeRows(ctx, mm, raw) }),
1120
+ findFirst: (args = {}) => defer(() => translateReadArgs(ctx, mm, args, 'first'), (qi, t) => qi.findFirst(t).then((r) => reshapeRowOrNull(ctx, mm, r)), { build: (qi, t) => qi.buildFindFirst(t), reshape: (raw) => reshapeRowOrNull(ctx, mm, raw) }),
1121
+ findUnique: (args) => defer(() => translateReadArgs(ctx, mm, requireWhere(args, 'findUnique'), 'unique'), (qi, t) => qi.findUnique(t).then((r) => reshapeRowOrNull(ctx, mm, r)), { build: (qi, t) => qi.buildFindUnique(t), reshape: (raw) => reshapeRowOrNull(ctx, mm, raw) }),
1122
+ findFirstOrThrow: (args = {}) => defer(() => translateReadArgs(ctx, mm, args, 'first'), (qi, t) => qi.findFirstOrThrow(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildFindFirstOrThrow(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
1123
+ findUniqueOrThrow: (args) => defer(() => translateReadArgs(ctx, mm, requireWhere(args, 'findUniqueOrThrow'), 'unique'), (qi, t) => qi.findUniqueOrThrow(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildFindUniqueOrThrow(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
1047
1124
  create: (args) => defer(() => {
1048
1125
  const t = { data: translateWriteData(ctx, mm, applyCreateDefaults(mm, args.data)) };
1049
1126
  if (typeof args.timeout === 'number')
@@ -1181,6 +1258,79 @@ function flattenTemplate(strings, values, ph) {
1181
1258
  // ---------------------------------------------------------------------------
1182
1259
  // createPrismaCompatClient
1183
1260
  // ---------------------------------------------------------------------------
1261
+ /**
1262
+ * The client-level members every compat client carries, as a total map over
1263
+ * {@link PrismaCompatClientBase}. Declaring it `Record<keyof …, true>` makes the
1264
+ * compiler keep it in sync: adding a `$method` to the interface without listing
1265
+ * it here is a type error, and listing one that does not exist is too. The
1266
+ * derived key set is what {@link junctionModels} refuses to shadow.
1267
+ */
1268
+ const CLIENT_RESERVED_KEY_MAP = {
1269
+ $transaction: true,
1270
+ $queryRaw: true,
1271
+ $queryRawUnsafe: true,
1272
+ $executeRaw: true,
1273
+ $executeRawUnsafe: true,
1274
+ $connect: true,
1275
+ $disconnect: true,
1276
+ };
1277
+ /** @internal Names on a compat client that are NOT model delegates. */
1278
+ export const CLIENT_RESERVED_KEYS = new Set(Object.keys(CLIENT_RESERVED_KEY_MAP));
1279
+ /** Warn-once namespace for junction accessors dropped on a name collision. */
1280
+ const JUNCTION_WARN_NS = 'prismaCompatJunction';
1281
+ /**
1282
+ * Synthesize a model map per implicit many-to-many JUNCTION table, keyed by the
1283
+ * junction's raw table name (`_ReportToSchedule`, `post_tags`, ...).
1284
+ *
1285
+ * Prisma's schema has no model for an implicit junction, so `PRISMA_MAP` has no
1286
+ * entry for it and the adapter would expose no accessor: a compat caller had no
1287
+ * way to touch link rows at all, not even inside a `$transaction`. The junction
1288
+ * tables ARE in the Turbine schema metadata (`through.table` on every
1289
+ * many-to-many relation), so the delegate is a plain identity mapping over the
1290
+ * real table: no field renames, no relations, no compound uniques.
1291
+ *
1292
+ * A junction accessor is an ESCAPE HATCH, never worth breaking a real member of
1293
+ * the client for, so it FAILS SAFE: the name is skipped (leaving the existing
1294
+ * member intact) whenever it collides with anything the client already exposes,
1295
+ * which is a Prisma model name, a table some model maps to, a model's
1296
+ * lowercased Prisma-property alias (`compat.user` for `model User`), or one of
1297
+ * the client-level {@link CLIENT_RESERVED_KEYS} (a junction table named
1298
+ * `$transaction` must not turn the transaction surface into a delegate).
1299
+ *
1300
+ * Every skip except the mapped-table one loses a capability the caller might
1301
+ * expect, so those warn once per junction name in dev; a table that a model
1302
+ * already maps to is not a loss (the model IS that table's accessor).
1303
+ */
1304
+ function junctionModels(ctx, map, tableToModel) {
1305
+ const out = [];
1306
+ const seen = new Set(CLIENT_RESERVED_KEYS);
1307
+ for (const model of Object.keys(map.models)) {
1308
+ seen.add(model);
1309
+ const alias = prismaPropertyAlias(model);
1310
+ if (alias)
1311
+ seen.add(alias);
1312
+ }
1313
+ for (const table of Object.values(ctx.schema.tables)) {
1314
+ for (const rel of Object.values(table.relations ?? {})) {
1315
+ if (rel.type !== 'manyToMany')
1316
+ continue;
1317
+ const name = rel.through?.table;
1318
+ if (!name || !ctx.schema.tables[name] || tableToModel.has(name))
1319
+ continue;
1320
+ if (seen.has(name)) {
1321
+ if (process.env.NODE_ENV !== 'production' && shouldWarnOnce(JUNCTION_WARN_NS, name)) {
1322
+ console.warn(`[turbine] prisma-compat: the many-to-many junction table "${name}" collides with an ` +
1323
+ 'existing client member of the same name, so no junction accessor was created for it. ' +
1324
+ 'Reach its rows through $queryRaw / $executeRaw, or through the owning model relation.');
1325
+ }
1326
+ continue;
1327
+ }
1328
+ seen.add(name);
1329
+ out.push([name, { table: name, accessor: name, fields: {}, relations: {}, compoundUniques: {} }]);
1330
+ }
1331
+ }
1332
+ return out;
1333
+ }
1184
1334
  /**
1185
1335
  * Create a PrismaClient-surface adapter over a {@link TurbineClient}, driven by a
1186
1336
  * {@link PrismaCompatMap} (the `prisma-map.ts` that `turbine
@@ -1216,9 +1366,17 @@ export function createPrismaCompatClient(client, map, options = {}) {
1216
1366
  prismaErrorCodes: options.prismaErrorCodes ?? false,
1217
1367
  },
1218
1368
  };
1369
+ // Every model the client exposes: the map's Prisma models, plus the implicit
1370
+ // many-to-many junction tables (see below). Both delegate loops (base and
1371
+ // transaction-scoped) build from this ONE list, so an accessor can never
1372
+ // exist on the client but be missing inside `$transaction`.
1373
+ const delegateModels = [
1374
+ ...Object.entries(map.models),
1375
+ ...junctionModels(ctx, map, tableToModel),
1376
+ ];
1219
1377
  // Delegates bound to the base client (each call reads db.table(...) lazily).
1220
1378
  const delegates = new Map();
1221
- for (const [prismaModel, mm] of Object.entries(map.models)) {
1379
+ for (const [prismaModel, mm] of delegateModels) {
1222
1380
  delegates.set(prismaModel, makeDelegate(ctx, mm, () => db.table(mm.table), (fn) => db.$transaction((tx) => fn((n) => tx.table(n)))));
1223
1381
  }
1224
1382
  const ph = placeholderOf(db);
@@ -1271,7 +1429,7 @@ export function createPrismaCompatClient(client, map, options = {}) {
1271
1429
  const fn = arg;
1272
1430
  return db.$transaction((tx) => {
1273
1431
  const txDelegates = {};
1274
- for (const [prismaModel, mm] of Object.entries(map.models)) {
1432
+ for (const [prismaModel, mm] of delegateModels) {
1275
1433
  txDelegates[prismaModel] = makeDelegate(ctx, mm, () => tx.table(mm.table), (fn) => fn((n) => tx.table(n)));
1276
1434
  const alias = prismaPropertyAlias(prismaModel);
1277
1435
  if (alias && !(alias in map.models) && !(alias in txDelegates)) {
@@ -11,6 +11,7 @@
11
11
  import { UnsupportedFeatureError, ValidationError } from '../errors.js';
12
12
  import { snakeToCamel } from '../schema.js';
13
13
  import { isJsonPathOrderBy, isVectorOrderBy, normalizeOrderBy, orderByEntries } from './filters.js';
14
+ import { ownLookup } from './utils.js';
14
15
  import * as whereMod from './where.js';
15
16
  /**
16
17
  * Enforce the PII contract on the aggregate surface. A PII-tagged
@@ -232,8 +233,8 @@ export function buildGroupBy(qi, args) {
232
233
  // no `$n` renumbering. `offset` without a deterministic `orderBy` yields an
233
234
  // arbitrary window (same caveat as findMany).
234
235
  if (args.limit !== undefined || args.offset !== undefined) {
235
- const limitPh = args.limit !== undefined ? qi.paginationRef(args.limit, params) : undefined;
236
- const offsetPh = args.offset !== undefined ? qi.paginationRef(args.offset, params) : undefined;
236
+ const limitPh = args.limit !== undefined ? qi.paginationRef(args.limit, params, 'limit') : undefined;
237
+ const offsetPh = args.offset !== undefined ? qi.paginationRef(args.offset, params, 'skip/offset') : undefined;
237
238
  sql += qi.buildPagination(limitPh, offsetPh, args.orderBy !== undefined);
238
239
  }
239
240
  return {
@@ -513,7 +514,10 @@ export function buildHavingClauses(qi, having, params, jsonAggExprs) {
513
514
  for (const [aggKey, filter] of Object.entries(value)) {
514
515
  if (filter === undefined)
515
516
  continue;
516
- const fn = aggFnByKey[aggKey];
517
+ // ownLookup, not a bare index: an inherited Object.prototype member
518
+ // ("constructor", "toString", …) would otherwise resolve to a truthy
519
+ // builtin and be spliced into the HAVING clause as its source text.
520
+ const fn = ownLookup(aggFnByKey, aggKey);
517
521
  if (!fn) {
518
522
  throw new ValidationError(`[turbine] Unknown aggregate "${aggKey}" in having for field "${key}" on table "${qi.table}". ` +
519
523
  `Supported: ${Object.keys(aggFnByKey).join(', ')}.`);