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
@@ -0,0 +1,209 @@
1
+ /**
2
+ * turbine-orm — Query builder utilities
3
+ *
4
+ * Standalone utility functions and classes used by the query builder.
5
+ */
6
+ /**
7
+ * Quote a SQL identifier (table name, column name) using Postgres double-quote
8
+ * rules: wrap in double quotes, escape internal double quotes by doubling them.
9
+ *
10
+ * @example
11
+ * quoteIdent('users') → '"users"'
12
+ * quoteIdent('my"table') → '"my""table"'
13
+ * quoteIdent('user name') → '"user name"'
14
+ */
15
+ export declare function quoteIdent(name: string): string;
16
+ /**
17
+ * Prototype-safe own-property read for the plain metadata maps (columnMap,
18
+ * relations, reverseColumnMap). These are constructed as plain objects, so a
19
+ * bare `map[key]` for a user-supplied field name like "constructor",
20
+ * "toString", or "__proto__" returns an inherited member from
21
+ * `Object.prototype` — a truthy value that slips past validation and produces a
22
+ * cryptic `TypeError` instead of a clean `ValidationError`. Returns `undefined`
23
+ * unless `key` is an OWN enumerable/non-enumerable property.
24
+ */
25
+ export declare function ownLookup<T>(map: Record<string, T>, key: string): T | undefined;
26
+ /**
27
+ * Escape single quotes for use as string keys in json_build_object().
28
+ * Doubles single quotes per SQL quoting rules.
29
+ */
30
+ export declare function escSingleQuote(s: string): string;
31
+ /**
32
+ * Escape LIKE pattern metacharacters: %, _, and \.
33
+ * Must be used with `ESCAPE '\'` in the LIKE clause.
34
+ */
35
+ export declare function escapeLike(value: string): string;
36
+ /**
37
+ * Simple LRU (Least Recently Used) cache with a fixed maximum size.
38
+ * When the cache exceeds maxSize, the oldest (least recently used) entry is evicted.
39
+ * Uses Map insertion order for O(1) eviction.
40
+ */
41
+ export declare class LRUCache<K, V> {
42
+ private maxSize;
43
+ private cache;
44
+ constructor(maxSize: number);
45
+ get(key: K): V | undefined;
46
+ set(key: K, value: V): void;
47
+ get size(): number;
48
+ }
49
+ /** Cached SQL template paired with its prepared-statement name. */
50
+ export interface SqlCacheEntry {
51
+ sql: string;
52
+ name: string;
53
+ }
54
+ /**
55
+ * FNV-1a 64-bit hash returning 16 lowercase hex chars.
56
+ * Single-loop string iteration. Uses BigInt for 64-bit math.
57
+ *
58
+ * @internal Exported for testing only.
59
+ */
60
+ export declare function fnv1a64Hex(s: string): string;
61
+ /**
62
+ * Derive a prepared-statement name from a SQL string.
63
+ * Format: `t_<16hex>` — always 18 chars, well under NAMEDATALEN (63).
64
+ *
65
+ * @internal Exported for testing only.
66
+ */
67
+ export declare function sqlToPreparedName(sql: string): string;
68
+ /** Known operator keys — used to detect operator objects vs plain values */
69
+ export declare const OPERATOR_KEYS: Set<string>;
70
+ /**
71
+ * Build a correlation clause joining columns between two table references.
72
+ * Handles both single-column (string) and multi-column (string[]) foreign keys.
73
+ *
74
+ * For single-column: `"alias"."col" = "parent"."col"`
75
+ * For multi-column: `"alias"."col_a" = "parent"."ref_a" AND "alias"."col_b" = "parent"."ref_b"`
76
+ */
77
+ export declare function buildCorrelation(leftRef: string, leftColumns: string | string[], rightRef: string, rightColumns: string | string[]): string;
78
+ /**
79
+ * Render a JS `Date` as a TIME-OF-DAY literal for a `time` / `timetz` column.
80
+ *
81
+ * Which time of day? The **UTC** components of the Date, never the process
82
+ * local zone. That is what Prisma does (`new Date('1970-01-01T09:00:00Z')`
83
+ * written to a `@db.Time(6)` column stores `09:00:00`), and the affected
84
+ * consumers are porting from Prisma, so Prisma is the contract. It is also the
85
+ * only choice that round-trips: the same Date produces the same literal no
86
+ * matter where the process runs.
87
+ *
88
+ * `timetz` gets an explicit `+00:00`, because the value's zone IS UTC and
89
+ * omitting it would let Postgres attach the session's `TimeZone` instead.
90
+ * Fractional seconds are emitted only when non-zero, so an even-second Date
91
+ * binds the plain `HH:MM:SS` form.
92
+ */
93
+ export declare function toTimeOfDayLiteral(value: Date, kind: 'time' | 'timetz'): string;
94
+ /** The temporal column shapes that need a bound Date rewritten to a literal. */
95
+ export type TemporalBindKind = 'time' | 'timetz' | 'date' | 'timestamp';
96
+ /**
97
+ * Render a JS `Date` as a literal for a zone-less `date` / `timestamp` column,
98
+ * using the value's **UTC** components.
99
+ *
100
+ * This is the write-side mirror of `parseDbDate`, which reads an offset-less
101
+ * database value back as UTC. Without it the driver serializes the Date with
102
+ * the PROCESS's offset (`prepareValue` → `dateToString`), so a `timestamp`
103
+ * column is not round-trip stable outside a UTC process: writing
104
+ * `2026-07-25T00:00Z` from `America/Los_Angeles` stores
105
+ * `2026-07-24 17:00:00` and reads back as `2026-07-24T17:00Z`. It also matches
106
+ * the choice {@link toTimeOfDayLiteral} already makes for `time` columns, and
107
+ * Prisma, which writes UTC components to zone-less columns.
108
+ *
109
+ * `timestamptz` is NOT handled here (and must not be): it stores a real
110
+ * instant, so the driver's local-offset string is already correct.
111
+ */
112
+ export declare function toLocalDateTimeLiteral(value: Date, kind: 'date' | 'timestamp'): string;
113
+ /**
114
+ * Classify a column's database type for temporal bind rewriting.
115
+ *
116
+ * `utcDateTimes: false` restricts the classification to the time-of-day types,
117
+ * whose rewrite is a hard-error fix (Postgres rejects an ISO timestamp for a
118
+ * `time` column outright) rather than a value correction.
119
+ */
120
+ export declare function temporalBindKind(dbType: string | undefined, utcDateTimes?: boolean): TemporalBindKind | null;
121
+ /**
122
+ * Rewrite one bound value for a temporal column: a JS `Date` on a `time` /
123
+ * `timetz` / `date` / `timestamp` column becomes the corresponding UTC literal,
124
+ * and an array of Dates on such a column is rewritten element-wise (the
125
+ * per-element rewrite is what a `time[]` column needs, and matches the scalar
126
+ * case rather than silently binding an ISO timestamp).
127
+ *
128
+ * Everything else — every non-Date, every non-temporal column, and every
129
+ * `timestamptz` column — is returned by IDENTITY, so this is a byte-for-byte
130
+ * no-op outside the shapes above.
131
+ */
132
+ export declare function coerceTemporalValue(dbType: string | undefined, value: unknown, utcDateTimes?: boolean): unknown;
133
+ /**
134
+ * Parse a database date-time string deterministically.
135
+ *
136
+ * Postgres `timestamp` (without time zone) values arrive with no offset —
137
+ * both from the driver and from `json_agg`/`json_build_object` subquery JSON
138
+ * (`2026-07-07T17:15:41.896`). JavaScript's `new Date()` interprets such
139
+ * strings in the SERVER'S LOCAL TIME ZONE, so the same row parses to a
140
+ * different instant depending on where the code runs. The universal ORM
141
+ * convention (Prisma, Rails, Django) is to treat offset-less timestamps as
142
+ * UTC — that is also the only interpretation that round-trips: Postgres
143
+ * stores exactly the wall-clock fields you sent.
144
+ *
145
+ * Strings that carry an explicit offset (`timestamptz` output) are parsed
146
+ * as-is.
147
+ */
148
+ export declare function parseDbDate(value: string): Date;
149
+ /**
150
+ * Postgres type name → OID, for every type family whose `json_build_object`
151
+ * rendering is NOT the value the pg driver produces for the same column.
152
+ *
153
+ * Why this table exists: the `'join'` strategy reads a relation through
154
+ * `json_agg(json_build_object(...))`, so its values are whatever
155
+ * `JSON.parse` makes of Postgres's JSON rendering. Every other read path in
156
+ * the library — a top-level row, `'batched'`, `'flatten'` — reads the column
157
+ * through the driver and gets the driver's representation. Measured against
158
+ * PostgreSQL 17, those two disagree for exactly the families below, which
159
+ * made the SAME query return a different JS type depending on which plan ran
160
+ * (and `'auto'` picks the plan from a row-count heuristic, so it could differ
161
+ * between two runs of one query). Three of these are lossy, not merely
162
+ * different:
163
+ *
164
+ * type driver (target) json_build_object
165
+ * ──────────── ──────────────────────────── ─────────────────────────────
166
+ * numeric '1000.50' (string) 1000.5 (number, LOSSY)
167
+ * int8 '9007199254740993' 9007199254740992 (LOSSY)
168
+ * bytea Buffer '\xdeadbeef' (string)
169
+ * date Date (local midnight) Date (UTC midnight, off by tz)
170
+ * interval { days, hours, … } '1 day 02:03:04' (string)
171
+ * point { x, y } '(1,2)' (string)
172
+ * circle { x, y, radius } '<(1,2),3>' (string)
173
+ *
174
+ * The array forms diverge the same way, plus `_timestamp`/`_timestamptz`
175
+ * (driver: `Date[]`; JSON: `string[]`) — the scalar `timestamp` /
176
+ * `timestamptz` are deliberately ABSENT because the existing `dateColumns`
177
+ * coercion in `parseRow` already lands them on the driver's value, and they
178
+ * are the hottest column type in a typical schema (no reason to add a cast to
179
+ * every `created_at`).
180
+ *
181
+ * The fix these OIDs drive: emit the column as `col::text` inside
182
+ * `json_build_object` so the JSON carries the same wire text the driver would
183
+ * receive, then run the DRIVER'S OWN parser for that OID over it. Parity is
184
+ * then by construction rather than by coincidence, and it automatically
185
+ * honours a caller's `pg.types.setTypeParser` (including the int8 parser
186
+ * TurbineClient itself registers) instead of second-guessing it.
187
+ *
188
+ * Postgres-only: the JSON functions and the divergence set are both
189
+ * engine-specific, so callers gate this on the postgres dialect.
190
+ */
191
+ export declare const JSON_WIRE_COERCION_OIDS: Readonly<Record<string, number>>;
192
+ /**
193
+ * The OID whose driver parser reproduces `pgType`'s driver representation from
194
+ * its text rendering, or `undefined` when the type's JSON rendering already
195
+ * matches the driver (the common case: text, uuid, bool, int4, float8, json,
196
+ * jsonb, arrays of those, …).
197
+ */
198
+ export declare function jsonWireCoercionOid(pgType: string | undefined): number | undefined;
199
+ /**
200
+ * Apply the driver's text parser for `oid` to a JSON-sourced wire string.
201
+ *
202
+ * Resolved through `pg.types.getTypeParser` on every call rather than
203
+ * memoized: parser registration is process-global and happens in the
204
+ * TurbineClient constructor (int8, and `timestamp` under `utcTimestamps`), and
205
+ * a caller may register their own at any point. A stale memo would silently
206
+ * reintroduce the very divergence this exists to remove. The lookup is a plain
207
+ * object index in pg-types, so it is not worth caching.
208
+ */
209
+ export declare function coerceJsonWireValue(oid: number, value: unknown): unknown;
@@ -4,8 +4,11 @@
4
4
  *
5
5
  * Standalone utility functions and classes used by the query builder.
6
6
  */
7
+ var __importDefault = (this && this.__importDefault) || function (mod) {
8
+ return (mod && mod.__esModule) ? mod : { "default": mod };
9
+ };
7
10
  Object.defineProperty(exports, "__esModule", { value: true });
8
- exports.OPERATOR_KEYS = exports.LRUCache = void 0;
11
+ exports.JSON_WIRE_COERCION_OIDS = exports.OPERATOR_KEYS = exports.LRUCache = void 0;
9
12
  exports.quoteIdent = quoteIdent;
10
13
  exports.ownLookup = ownLookup;
11
14
  exports.escSingleQuote = escSingleQuote;
@@ -13,7 +16,15 @@ exports.escapeLike = escapeLike;
13
16
  exports.fnv1a64Hex = fnv1a64Hex;
14
17
  exports.sqlToPreparedName = sqlToPreparedName;
15
18
  exports.buildCorrelation = buildCorrelation;
19
+ exports.toTimeOfDayLiteral = toTimeOfDayLiteral;
20
+ exports.toLocalDateTimeLiteral = toLocalDateTimeLiteral;
21
+ exports.temporalBindKind = temporalBindKind;
22
+ exports.coerceTemporalValue = coerceTemporalValue;
16
23
  exports.parseDbDate = parseDbDate;
24
+ exports.jsonWireCoercionOid = jsonWireCoercionOid;
25
+ exports.coerceJsonWireValue = coerceJsonWireValue;
26
+ const pg_1 = __importDefault(require("pg"));
27
+ const schema_js_1 = require("../schema.js");
17
28
  // ---------------------------------------------------------------------------
18
29
  // Identifier quoting — prevents SQL injection via table/column names
19
30
  // ---------------------------------------------------------------------------
@@ -153,6 +164,114 @@ function buildCorrelation(leftRef, leftColumns, rightRef, rightColumns) {
153
164
  .map((col, i) => `${leftRef}.${quoteIdent(col)} = ${rightRef}.${quoteIdent(rightCols[i])}`)
154
165
  .join(' AND ');
155
166
  }
167
+ /**
168
+ * Render a JS `Date` as a TIME-OF-DAY literal for a `time` / `timetz` column.
169
+ *
170
+ * Which time of day? The **UTC** components of the Date, never the process
171
+ * local zone. That is what Prisma does (`new Date('1970-01-01T09:00:00Z')`
172
+ * written to a `@db.Time(6)` column stores `09:00:00`), and the affected
173
+ * consumers are porting from Prisma, so Prisma is the contract. It is also the
174
+ * only choice that round-trips: the same Date produces the same literal no
175
+ * matter where the process runs.
176
+ *
177
+ * `timetz` gets an explicit `+00:00`, because the value's zone IS UTC and
178
+ * omitting it would let Postgres attach the session's `TimeZone` instead.
179
+ * Fractional seconds are emitted only when non-zero, so an even-second Date
180
+ * binds the plain `HH:MM:SS` form.
181
+ */
182
+ function toTimeOfDayLiteral(value, kind) {
183
+ const pad = (n, width = 2) => String(n).padStart(width, '0');
184
+ const ms = value.getUTCMilliseconds();
185
+ const literal = `${pad(value.getUTCHours())}:${pad(value.getUTCMinutes())}:${pad(value.getUTCSeconds())}` +
186
+ (ms === 0 ? '' : `.${pad(ms, 3)}`);
187
+ return kind === 'timetz' ? `${literal}+00:00` : literal;
188
+ }
189
+ /** Render the UTC calendar date of a `Date` as `YYYY-MM-DD`. */
190
+ function utcDatePart(value) {
191
+ const year = value.getUTCFullYear();
192
+ const y = year < 0 ? `-${String(-year).padStart(4, '0')}` : String(year).padStart(4, '0');
193
+ const pad = (n) => String(n).padStart(2, '0');
194
+ return `${y}-${pad(value.getUTCMonth() + 1)}-${pad(value.getUTCDate())}`;
195
+ }
196
+ /**
197
+ * Render a JS `Date` as a literal for a zone-less `date` / `timestamp` column,
198
+ * using the value's **UTC** components.
199
+ *
200
+ * This is the write-side mirror of `parseDbDate`, which reads an offset-less
201
+ * database value back as UTC. Without it the driver serializes the Date with
202
+ * the PROCESS's offset (`prepareValue` → `dateToString`), so a `timestamp`
203
+ * column is not round-trip stable outside a UTC process: writing
204
+ * `2026-07-25T00:00Z` from `America/Los_Angeles` stores
205
+ * `2026-07-24 17:00:00` and reads back as `2026-07-24T17:00Z`. It also matches
206
+ * the choice {@link toTimeOfDayLiteral} already makes for `time` columns, and
207
+ * Prisma, which writes UTC components to zone-less columns.
208
+ *
209
+ * `timestamptz` is NOT handled here (and must not be): it stores a real
210
+ * instant, so the driver's local-offset string is already correct.
211
+ */
212
+ function toLocalDateTimeLiteral(value, kind) {
213
+ const datePart = utcDatePart(value);
214
+ if (kind === 'date')
215
+ return datePart;
216
+ return `${datePart} ${toTimeOfDayLiteral(value, 'time')}`;
217
+ }
218
+ /**
219
+ * Classify a column's database type for temporal bind rewriting.
220
+ *
221
+ * `utcDateTimes: false` restricts the classification to the time-of-day types,
222
+ * whose rewrite is a hard-error fix (Postgres rejects an ISO timestamp for a
223
+ * `time` column outright) rather than a value correction.
224
+ */
225
+ function temporalBindKind(dbType, utcDateTimes = true) {
226
+ const timeKind = (0, schema_js_1.timeOfDayKind)(dbType);
227
+ if (timeKind)
228
+ return timeKind;
229
+ return utcDateTimes ? (0, schema_js_1.localDateTimeKind)(dbType) : null;
230
+ }
231
+ /**
232
+ * Rewrite one bound value for a temporal column: a JS `Date` on a `time` /
233
+ * `timetz` / `date` / `timestamp` column becomes the corresponding UTC literal,
234
+ * and an array of Dates on such a column is rewritten element-wise (the
235
+ * per-element rewrite is what a `time[]` column needs, and matches the scalar
236
+ * case rather than silently binding an ISO timestamp).
237
+ *
238
+ * Everything else — every non-Date, every non-temporal column, and every
239
+ * `timestamptz` column — is returned by IDENTITY, so this is a byte-for-byte
240
+ * no-op outside the shapes above.
241
+ */
242
+ function coerceTemporalValue(dbType, value, utcDateTimes = true) {
243
+ const isDate = value instanceof Date;
244
+ if (!isDate && !Array.isArray(value))
245
+ return value;
246
+ if (isDate && Number.isNaN(value.getTime()))
247
+ return value;
248
+ // An array value is either an `in`/`notIn` list on a scalar temporal column
249
+ // (type already the element type) or the value of an array column, whose
250
+ // introspected type is the `_time` / `_timestamp` array spelling.
251
+ const kind = temporalBindKind(isDate ? dbType : arrayElementDbType(dbType), utcDateTimes);
252
+ if (!kind)
253
+ return value;
254
+ if (isDate)
255
+ return renderTemporal(value, kind);
256
+ // Rewrite only if the list actually holds a Date, so a string list stays
257
+ // byte-identical (and the same array instance is returned).
258
+ if (!value.some((v) => v instanceof Date))
259
+ return value;
260
+ return value.map((v) => (v instanceof Date && !Number.isNaN(v.getTime()) ? renderTemporal(v, kind) : v));
261
+ }
262
+ /** `_time` → `time`, `time[]` → `time`, anything else unchanged. */
263
+ function arrayElementDbType(dbType) {
264
+ if (!dbType)
265
+ return dbType;
266
+ if (dbType.startsWith('_'))
267
+ return dbType.slice(1);
268
+ return dbType.endsWith('[]') ? dbType.slice(0, -2) : dbType;
269
+ }
270
+ function renderTemporal(value, kind) {
271
+ return kind === 'date' || kind === 'timestamp'
272
+ ? toLocalDateTimeLiteral(value, kind)
273
+ : toTimeOfDayLiteral(value, kind);
274
+ }
156
275
  /**
157
276
  * Matches an explicit timezone suffix on a date-time string: a trailing `Z`
158
277
  * or a `±HH`, `±HHMM`, `±HH:MM` offset.
@@ -188,3 +307,91 @@ function parseDbDate(value) {
188
307
  // normalize `YYYY-MM-DD HH:MM:SS` (driver form) to ISO before pinning UTC
189
308
  return new Date(`${value.replace(' ', 'T')}Z`);
190
309
  }
310
+ // ---------------------------------------------------------------------------
311
+ // JSON-wire value coercion (relationLoadStrategy: 'join')
312
+ // ---------------------------------------------------------------------------
313
+ /**
314
+ * Postgres type name → OID, for every type family whose `json_build_object`
315
+ * rendering is NOT the value the pg driver produces for the same column.
316
+ *
317
+ * Why this table exists: the `'join'` strategy reads a relation through
318
+ * `json_agg(json_build_object(...))`, so its values are whatever
319
+ * `JSON.parse` makes of Postgres's JSON rendering. Every other read path in
320
+ * the library — a top-level row, `'batched'`, `'flatten'` — reads the column
321
+ * through the driver and gets the driver's representation. Measured against
322
+ * PostgreSQL 17, those two disagree for exactly the families below, which
323
+ * made the SAME query return a different JS type depending on which plan ran
324
+ * (and `'auto'` picks the plan from a row-count heuristic, so it could differ
325
+ * between two runs of one query). Three of these are lossy, not merely
326
+ * different:
327
+ *
328
+ * type driver (target) json_build_object
329
+ * ──────────── ──────────────────────────── ─────────────────────────────
330
+ * numeric '1000.50' (string) 1000.5 (number, LOSSY)
331
+ * int8 '9007199254740993' 9007199254740992 (LOSSY)
332
+ * bytea Buffer '\xdeadbeef' (string)
333
+ * date Date (local midnight) Date (UTC midnight, off by tz)
334
+ * interval { days, hours, … } '1 day 02:03:04' (string)
335
+ * point { x, y } '(1,2)' (string)
336
+ * circle { x, y, radius } '<(1,2),3>' (string)
337
+ *
338
+ * The array forms diverge the same way, plus `_timestamp`/`_timestamptz`
339
+ * (driver: `Date[]`; JSON: `string[]`) — the scalar `timestamp` /
340
+ * `timestamptz` are deliberately ABSENT because the existing `dateColumns`
341
+ * coercion in `parseRow` already lands them on the driver's value, and they
342
+ * are the hottest column type in a typical schema (no reason to add a cast to
343
+ * every `created_at`).
344
+ *
345
+ * The fix these OIDs drive: emit the column as `col::text` inside
346
+ * `json_build_object` so the JSON carries the same wire text the driver would
347
+ * receive, then run the DRIVER'S OWN parser for that OID over it. Parity is
348
+ * then by construction rather than by coincidence, and it automatically
349
+ * honours a caller's `pg.types.setTypeParser` (including the int8 parser
350
+ * TurbineClient itself registers) instead of second-guessing it.
351
+ *
352
+ * Postgres-only: the JSON functions and the divergence set are both
353
+ * engine-specific, so callers gate this on the postgres dialect.
354
+ */
355
+ exports.JSON_WIRE_COERCION_OIDS = {
356
+ numeric: 1700,
357
+ int8: 20,
358
+ bytea: 17,
359
+ date: 1082,
360
+ interval: 1186,
361
+ point: 600,
362
+ circle: 718,
363
+ _numeric: 1231,
364
+ _int8: 1016,
365
+ _bytea: 1001,
366
+ _date: 1182,
367
+ _interval: 1187,
368
+ _point: 1017,
369
+ _timestamp: 1115,
370
+ _timestamptz: 1185,
371
+ };
372
+ /**
373
+ * The OID whose driver parser reproduces `pgType`'s driver representation from
374
+ * its text rendering, or `undefined` when the type's JSON rendering already
375
+ * matches the driver (the common case: text, uuid, bool, int4, float8, json,
376
+ * jsonb, arrays of those, …).
377
+ */
378
+ function jsonWireCoercionOid(pgType) {
379
+ if (!pgType)
380
+ return undefined;
381
+ return exports.JSON_WIRE_COERCION_OIDS[pgType];
382
+ }
383
+ /**
384
+ * Apply the driver's text parser for `oid` to a JSON-sourced wire string.
385
+ *
386
+ * Resolved through `pg.types.getTypeParser` on every call rather than
387
+ * memoized: parser registration is process-global and happens in the
388
+ * TurbineClient constructor (int8, and `timestamp` under `utcTimestamps`), and
389
+ * a caller may register their own at any point. A stale memo would silently
390
+ * reintroduce the very divergence this exists to remove. The lookup is a plain
391
+ * object index in pg-types, so it is not worth caching.
392
+ */
393
+ function coerceJsonWireValue(oid, value) {
394
+ if (typeof value !== 'string')
395
+ return value;
396
+ return pg_1.default.types.getTypeParser(oid, 'text')(value);
397
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * turbine-orm, process-wide once-per-key dev-warning dedupe registry.
3
+ *
4
+ * Several dev-only diagnostics (the missing-FK-index warning in relations.ts,
5
+ * the `relationLoadStrategy: 'auto'` engagement note, the deep-`with` warning)
6
+ * must fire AT MOST ONCE per distinct key for the life of the process. A
7
+ * module-level `Set` almost does this, but it is defeated by the two field
8
+ * realities this package actually ships into:
9
+ *
10
+ * 1. **Dual-package loading.** Turbine ships ESM (`dist/`) AND CJS
11
+ * (`dist/cjs/`). A mixed `require`/`import` graph (a compat layer, a tool
12
+ * that loads both) instantiates the module twice, giving two independent
13
+ * `Set`s that each warn once, a double warning.
14
+ * 2. **Bundler / HMR re-evaluation.** Under Next.js dev the module is
15
+ * re-evaluated per recompile, resetting a module-level `Set` and making the
16
+ * warning appear to fire every time.
17
+ *
18
+ * Hanging the registry off `globalThis` under a `Symbol.for(...)` key gives every
19
+ * module copy in the realm ONE shared registry (cross-copy identity without
20
+ * polluting enumerable globals), and `globalThis` survives webpack recompiles
21
+ * because the realm persists, which is exactly what fixes the every-recompile
22
+ * firing in dev servers. Per-process firing (worker threads, separate processes)
23
+ * is acceptable and stays.
24
+ *
25
+ * Bounded: each namespace stops recording AND stops warning once it reaches
26
+ * {@link WARN_ONCE_CAP} distinct keys. A schema with 500+ distinct unindexed
27
+ * relations has long since gotten the message, and the cap prevents unbounded
28
+ * growth if metadata objects are churned dynamically. (Clearing on overflow
29
+ * would be wrong, it would re-warn.)
30
+ */
31
+ /** Per-namespace cap on distinct recorded keys (see module doc). */
32
+ export declare const WARN_ONCE_CAP = 500;
33
+ /**
34
+ * Record `(ns, key)` and report whether THIS call is the first to see it
35
+ * process-wide. Returns `true` exactly once per distinct key (the caller should
36
+ * emit its warning then), `false` on every subsequent call for that key, and
37
+ * `false` once the namespace has recorded {@link WARN_ONCE_CAP} distinct keys
38
+ * (bounded growth; the warning simply stops rather than re-firing).
39
+ */
40
+ export declare function shouldWarnOnce(ns: string, key: string): boolean;
41
+ /** True when `(ns, key)` has already been recorded (no mutation). */
42
+ export declare function hasWarnedOnce(ns: string, key: string): boolean;
43
+ /**
44
+ * @internal Test-only: clear one namespace, or the whole registry when `ns` is
45
+ * omitted. Lets a single test process verify that a warning fires once and then
46
+ * re-verify after a reset without spawning a new process.
47
+ */
48
+ export declare function resetWarnOnce(ns?: string): void;
49
+ /** Namespace constants so callers never typo a bare string. */
50
+ export declare const WARN_NS: {
51
+ /** Missing-FK-index runtime warning (relations.ts `buildRelationSubquery`). */
52
+ readonly unindexedRelation: "unindexedRelation";
53
+ /** `relationLoadStrategy: 'auto'` batched-fallback engagement note. */
54
+ readonly autoStrategy: "autoStrategy";
55
+ /** Deep-`with` (depth > 5) advisory (builder.ts `findMany`). */
56
+ readonly deepWith: "deepWith";
57
+ /** Non-deterministic page advisory: paginating findMany with no orderBy. */
58
+ readonly unorderedPage: "unorderedPage";
59
+ /** PowDB `emitLinks` DDL skips (name/column collision, endpoint drift). */
60
+ readonly powdbLinks: "powdbLinks";
61
+ /**
62
+ * `relationLoadStrategy: 'flatten'` was asked for but a relation stayed on the
63
+ * correlated-subquery path (relations.ts `planFlattenWith`, builder.ts
64
+ * `planFlatten`). An explicitly requested strategy that quietly does not
65
+ * engage is indistinguishable from one that does nothing, so say so once.
66
+ */
67
+ readonly flattenFallback: "flattenFallback";
68
+ };
@@ -95,6 +95,15 @@ exports.WARN_NS = {
95
95
  autoStrategy: 'autoStrategy',
96
96
  /** Deep-`with` (depth > 5) advisory (builder.ts `findMany`). */
97
97
  deepWith: 'deepWith',
98
+ /** Non-deterministic page advisory: paginating findMany with no orderBy. */
99
+ unorderedPage: 'unorderedPage',
98
100
  /** PowDB `emitLinks` DDL skips (name/column collision, endpoint drift). */
99
101
  powdbLinks: 'powdbLinks',
102
+ /**
103
+ * `relationLoadStrategy: 'flatten'` was asked for but a relation stayed on the
104
+ * correlated-subquery path (relations.ts `planFlattenWith`, builder.ts
105
+ * `planFlatten`). An explicitly requested strategy that quietly does not
106
+ * engage is indistinguishable from one that does nothing, so say so once.
107
+ */
108
+ flattenFallback: 'flattenFallback',
100
109
  };
@@ -0,0 +1,139 @@
1
+ /**
2
+ * turbine-orm: Shared WHERE-clause walk
3
+ *
4
+ * The SQL template cache requires three code paths over a table-scoped WHERE
5
+ * object to stay in perfect lockstep:
6
+ * - `fingerprintWhere` : the value-invariant cache KEY,
7
+ * - `buildWhereClause` : the SQL text + `$N` params on a cache MISS,
8
+ * - `collectWhereParams` : the params ONLY on a cache HIT (no SQL rebuild).
9
+ *
10
+ * If any two of them enumerate the WHERE keys in a different order, or classify
11
+ * a key's value into a different filter shape, the cached SQL's `$N`
12
+ * placeholders bind the wrong values: a silent cross-value (and, with tenant
13
+ * columns, cross-tenant) leak. That drift shipped twice historically (permuted
14
+ * where-key order; an orderBy fingerprint collision).
15
+ *
16
+ * This module removes the drift BY CONSTRUCTION:
17
+ * - {@link walkWhere} is the ONE enumeration. It sorts keys canonically,
18
+ * skips `undefined`, dispatches the `OR`/`AND`/`NOT` combinators and
19
+ * relation filters, and yields a flat, ordered {@link WhereEvent} stream.
20
+ * All three consumers iterate this same stream, so their key order and
21
+ * combinator structure can never diverge again.
22
+ * - {@link classifyScalarForSql} is the ONE scalar-shape decision the SQL
23
+ * paths use. `buildWhereClause` and `collectWhereParams` BOTH call it with
24
+ * the same `(rawColumn, value)`, so they always take the same branch and
25
+ * therefore push params in the same order.
26
+ * - {@link fingerprintScalarToken} is the fingerprint's own (deliberately
27
+ * column-blind) scalar token. It over-distinguishes relative to the SQL
28
+ * classifier (which is always safe), so a fingerprint match still implies
29
+ * an identical SQL shape.
30
+ *
31
+ * The dev-mode / sampled-production cross-check in `builder.ts` stays as the
32
+ * tripwire: with this shared walk it should never fire, but it remains the
33
+ * last-line guard against a future leaf builder / collect mirror falling out of
34
+ * step.
35
+ */
36
+ import type { RelationDef, TableMetadata } from '../schema.js';
37
+ /** A table-scoped WHERE object (or an `OR`/`AND`/`NOT` branch of one). */
38
+ export type WhereRecord = Record<string, unknown>;
39
+ /**
40
+ * Everything the shared walk needs from the owning {@link QueryInterface}. Bound
41
+ * once per instance (see `QueryInterface`'s `whereHost` field) so the walk stays
42
+ * a pure function of the instance's schema state without widening the class's
43
+ * public surface.
44
+ */
45
+ export interface WhereHost {
46
+ readonly tableMeta: TableMetadata;
47
+ /** Wrap a bare belongsTo/hasOne relation filter in `{ is: … }`. */
48
+ normalizeRelationFilter(relDef: RelationDef, filterObj: WhereRecord): WhereRecord;
49
+ /** Resolve a column's Postgres type token (defaults to `text`). */
50
+ getColumnPgType(column: string): string;
51
+ /** True for `json` / `jsonb` column types. */
52
+ isJsonColumnType(colType: string): boolean;
53
+ }
54
+ /**
55
+ * The SQL-relevant classification of a scalar WHERE value, decided column-aware
56
+ * in the SAME linear fall-through order the SQL builder has always used
57
+ * (null → vector → json → array → text-search → operator → equality). Shared by
58
+ * the build and collect paths so their branch choice (and thus param order)
59
+ * is identical by construction. The `*Throw` variants preserve the build path's
60
+ * strict-validation errors for a JSON/array operator on a non-JSON/array column.
61
+ */
62
+ export type ScalarSqlClass = {
63
+ kind: 'null';
64
+ } | {
65
+ kind: 'vector';
66
+ } | {
67
+ kind: 'json';
68
+ } | {
69
+ kind: 'jsonThrow';
70
+ jsonKey: string;
71
+ } | {
72
+ kind: 'array';
73
+ colType: string;
74
+ } | {
75
+ kind: 'arrayThrow';
76
+ arrayKey: string;
77
+ } | {
78
+ kind: 'textsearch';
79
+ } | {
80
+ kind: 'operator';
81
+ } | {
82
+ kind: 'equality';
83
+ };
84
+ /**
85
+ * One node of the canonical WHERE walk. `scalar` carries only `key` + `value`;
86
+ * each consumer resolves the column/shape itself (the SQL paths via
87
+ * {@link classifyScalarForSql}, the fingerprint via
88
+ * {@link fingerprintScalarToken}) so the fingerprint stays column-blind exactly
89
+ * as before.
90
+ */
91
+ export type WhereEvent = {
92
+ kind: 'or';
93
+ conditions: WhereRecord[];
94
+ } | {
95
+ kind: 'and';
96
+ conditions: WhereRecord[];
97
+ } | {
98
+ kind: 'not';
99
+ condition: WhereRecord;
100
+ } | {
101
+ kind: 'relation';
102
+ key: string;
103
+ relDef: RelationDef;
104
+ filterObj: WhereRecord;
105
+ } | {
106
+ kind: 'scalar';
107
+ key: string;
108
+ value: unknown;
109
+ };
110
+ /**
111
+ * THE canonical WHERE enumeration. Yields events in sorted-key order (skipping
112
+ * `undefined`), dispatching combinators and relation filters, so every consumer
113
+ * (fingerprint, SQL build, param collect) walks identically.
114
+ *
115
+ * Combinator SKIP rules match the historical code exactly: an `OR`/`AND` whose
116
+ * value is a non-array or an empty array is skipped entirely (it contributes
117
+ * neither SQL, params, nor a fingerprint token); `NOT` is always emitted.
118
+ * A key that names a relation but whose value is not a `{ some/every/none/is/
119
+ * isNot }` filter falls through to the scalar path, exactly as before.
120
+ */
121
+ export declare function walkWhere(host: WhereHost, where: WhereRecord): WhereEvent[];
122
+ /**
123
+ * Column-aware SQL classification of a scalar WHERE value. Reproduces the SQL
124
+ * builder's linear fall-through: a JSON/array-shaped value on a non-JSON/array
125
+ * column falls THROUGH to the next shape (and ultimately equality) unless it
126
+ * carries a shape-unique key, in which case the build path reports a typed
127
+ * error (`*Throw`). Both the build and collect paths call this, so they can
128
+ * never classify the same value differently.
129
+ */
130
+ export declare function classifyScalarForSql(host: WhereHost, rawColumn: string, value: unknown): ScalarSqlClass;
131
+ /**
132
+ * The fingerprint's scalar token: deliberately COLUMN-BLIND and in the
133
+ * historical fingerprint precedence (operator before vector/json/array), so a
134
+ * value that both looks like an operator and a JSON filter (`equals`/`contains`
135
+ * overlap) tokenizes as an operator exactly as it did before. Column-blindness
136
+ * only ever over-distinguishes versus {@link classifyScalarForSql}, which is
137
+ * safe: it can cause an extra cache MISS, never a wrong-value HIT.
138
+ */
139
+ export declare function fingerprintScalarToken(value: unknown): string;