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.
- package/README.md +122 -39
- package/dist/cjs/adapters/cockroachdb.d.ts +39 -0
- package/dist/cjs/adapters/index.d.ts +110 -0
- package/dist/cjs/adapters/yugabytedb.d.ts +51 -0
- package/dist/cjs/cli/config.d.ts +181 -0
- package/dist/cjs/cli/config.js +32 -6
- package/dist/cjs/cli/destructive.d.ts +38 -0
- package/dist/cjs/cli/index.d.ts +359 -0
- package/dist/cjs/cli/index.js +228 -56
- package/dist/cjs/cli/loader.d.ts +61 -0
- package/dist/cjs/cli/mcp.d.ts +42 -0
- package/dist/cjs/cli/migrate.d.ts +356 -0
- package/dist/cjs/cli/migrate.js +131 -40
- package/dist/cjs/cli/observe-ui.d.ts +1 -0
- package/dist/cjs/cli/observe-ui.js +14 -5
- package/dist/cjs/cli/observe.d.ts +25 -0
- package/dist/cjs/cli/observe.js +49 -12
- package/dist/cjs/cli/pii-tags.d.ts +53 -0
- package/dist/cjs/cli/prisma-report.d.ts +33 -0
- package/dist/cjs/cli/prisma-report.js +73 -0
- package/dist/cjs/cli/prisma-resolve.d.ts +106 -0
- package/dist/cjs/cli/prisma-resolve.js +1 -0
- package/dist/cjs/cli/prisma-schema.d.ts +176 -0
- package/dist/cjs/cli/prisma-schema.js +82 -4
- package/dist/cjs/cli/rate-limit.d.ts +32 -0
- package/dist/cjs/cli/rate-limit.js +45 -0
- package/dist/cjs/cli/studio-demo.d.ts +43 -0
- package/dist/cjs/cli/studio-ui.generated.d.ts +1 -0
- package/dist/cjs/cli/studio.d.ts +207 -0
- package/dist/cjs/cli/studio.js +136 -71
- package/dist/cjs/cli/ui.d.ts +73 -0
- package/dist/cjs/cli/ui.js +51 -9
- package/dist/cjs/client.d.ts +837 -0
- package/dist/cjs/client.js +3 -0
- package/dist/cjs/dialect.d.ts +516 -0
- package/dist/cjs/dialect.js +37 -12
- package/dist/cjs/errors.d.ts +370 -0
- package/dist/cjs/generate.d.ts +137 -0
- package/dist/cjs/generate.js +39 -6
- package/dist/cjs/index-advisor.d.ts +153 -0
- package/dist/cjs/index-stats.d.ts +384 -0
- package/dist/cjs/index.d.ts +55 -0
- package/dist/cjs/index.js +7 -2
- package/dist/cjs/introspect.d.ts +269 -0
- package/dist/cjs/mssql.d.ts +232 -0
- package/dist/cjs/mssql.js +6 -0
- package/dist/cjs/mysql.d.ts +173 -0
- package/dist/cjs/mysql.js +16 -0
- package/dist/cjs/nested-write.d.ts +96 -0
- package/dist/cjs/nested-write.js +414 -24
- package/dist/cjs/observe.d.ts +115 -0
- package/dist/cjs/optional-peer-import.d.cts +72 -0
- package/dist/cjs/pipeline-submittable.d.ts +93 -0
- package/dist/cjs/pipeline.d.ts +71 -0
- package/dist/cjs/powdb-introspect.d.ts +84 -0
- package/dist/cjs/powdb.d.ts +931 -0
- package/dist/cjs/powdb.js +106 -21
- package/dist/cjs/powql.d.ts +592 -0
- package/dist/cjs/powql.js +42 -6
- package/dist/cjs/prisma-compat.d.ts +283 -0
- package/dist/cjs/prisma-compat.js +167 -9
- package/dist/cjs/query/aggregates.d.ts +92 -0
- package/dist/cjs/query/aggregates.js +7 -3
- package/dist/cjs/query/batched-loader.d.ts +193 -0
- package/dist/cjs/query/builder.d.ts +849 -0
- package/dist/cjs/query/builder.js +571 -65
- package/dist/cjs/query/compound-unique.d.ts +51 -0
- package/dist/cjs/query/deferred.d.ts +223 -0
- package/dist/cjs/query/filters.d.ts +201 -0
- package/dist/cjs/query/index.d.ts +14 -0
- package/dist/cjs/query/index.js +6 -1
- package/dist/cjs/query/relations.d.ts +609 -0
- package/dist/cjs/query/relations.js +693 -46
- package/dist/cjs/query/types.d.ts +1300 -0
- package/dist/cjs/query/utils.d.ts +209 -0
- package/dist/cjs/query/utils.js +208 -1
- package/dist/cjs/query/warn-registry.d.ts +68 -0
- package/dist/cjs/query/warn-registry.js +9 -0
- package/dist/cjs/query/where-compile.d.ts +139 -0
- package/dist/cjs/query/where.d.ts +548 -0
- package/dist/cjs/query/where.js +58 -22
- package/dist/cjs/query/writes.d.ts +172 -0
- package/dist/cjs/query/writes.js +105 -12
- package/dist/cjs/realtime.d.ts +70 -0
- package/dist/cjs/schema-builder.d.ts +354 -0
- package/dist/cjs/schema-metadata.d.ts +83 -0
- package/dist/cjs/schema-sql.d.ts +217 -0
- package/dist/cjs/schema-sql.js +23 -5
- package/dist/cjs/schema.d.ts +356 -0
- package/dist/cjs/schema.js +125 -0
- package/dist/cjs/seed.d.ts +15 -0
- package/dist/cjs/serverless.d.ts +142 -0
- package/dist/cjs/sqlite.d.ts +143 -0
- package/dist/cjs/sqlite.js +4 -0
- package/dist/cjs/typed-sql.d.ts +102 -0
- package/dist/cli/config.d.ts +18 -4
- package/dist/cli/config.js +31 -6
- package/dist/cli/index.d.ts +123 -0
- package/dist/cli/index.js +223 -58
- package/dist/cli/migrate.d.ts +59 -10
- package/dist/cli/migrate.js +128 -41
- package/dist/cli/observe-ui.d.ts +1 -1
- package/dist/cli/observe-ui.js +14 -5
- package/dist/cli/observe.d.ts +7 -1
- package/dist/cli/observe.js +48 -12
- package/dist/cli/prisma-report.d.ts +14 -0
- package/dist/cli/prisma-report.js +72 -0
- package/dist/cli/prisma-resolve.d.ts +6 -0
- package/dist/cli/prisma-resolve.js +1 -0
- package/dist/cli/prisma-schema.d.ts +62 -2
- package/dist/cli/prisma-schema.js +81 -4
- package/dist/cli/rate-limit.d.ts +32 -0
- package/dist/cli/rate-limit.js +40 -0
- package/dist/cli/studio.d.ts +5 -5
- package/dist/cli/studio.js +135 -70
- package/dist/cli/ui.d.ts +1 -1
- package/dist/cli/ui.js +51 -9
- package/dist/client.d.ts +40 -0
- package/dist/client.js +3 -0
- package/dist/dialect.d.ts +17 -1
- package/dist/dialect.js +37 -12
- package/dist/generate.js +40 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mssql.js +6 -0
- package/dist/mysql.js +16 -0
- package/dist/nested-write.d.ts +2 -0
- package/dist/nested-write.js +415 -25
- package/dist/powdb.d.ts +4 -2
- package/dist/powdb.js +106 -21
- package/dist/powql.d.ts +5 -0
- package/dist/powql.js +42 -6
- package/dist/prisma-compat.d.ts +2 -0
- package/dist/prisma-compat.js +166 -8
- package/dist/query/aggregates.js +7 -3
- package/dist/query/builder.d.ts +292 -21
- package/dist/query/builder.js +570 -64
- package/dist/query/deferred.d.ts +39 -0
- package/dist/query/index.d.ts +1 -1
- package/dist/query/index.js +1 -1
- package/dist/query/relations.d.ts +173 -5
- package/dist/query/relations.js +688 -47
- package/dist/query/types.d.ts +123 -39
- package/dist/query/utils.d.ts +116 -0
- package/dist/query/utils.js +198 -0
- package/dist/query/warn-registry.d.ts +9 -0
- package/dist/query/warn-registry.js +9 -0
- package/dist/query/where.d.ts +38 -1
- package/dist/query/where.js +58 -23
- package/dist/query/writes.d.ts +42 -1
- package/dist/query/writes.js +104 -13
- package/dist/schema-sql.d.ts +14 -0
- package/dist/schema-sql.js +23 -5
- package/dist/schema.d.ts +38 -0
- package/dist/schema.js +123 -0
- package/dist/sqlite.js +4 -0
- 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;
|
package/dist/cjs/query/utils.js
CHANGED
|
@@ -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;
|