turbine-orm 0.34.0 → 0.36.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 +18 -16
- package/dist/cjs/cli/index.js +109 -16
- package/dist/cjs/cli/migrate.js +78 -3
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.js +333 -22
- package/dist/cjs/cli/ui.js +7 -1
- package/dist/cjs/client.js +26 -4
- package/dist/cjs/dialect.js +2 -1
- package/dist/cjs/errors.js +41 -1
- package/dist/cjs/generate.js +23 -2
- package/dist/cjs/index.js +4 -2
- package/dist/cjs/mssql.js +27 -5
- package/dist/cjs/mysql.js +4 -0
- package/dist/cjs/powdb.js +197 -25
- package/dist/cjs/powql.js +515 -51
- package/dist/cjs/query/aggregates.js +683 -0
- package/dist/cjs/query/batched-loader.js +2 -0
- package/dist/cjs/query/builder.js +361 -4508
- package/dist/cjs/query/filters.js +12 -0
- package/dist/cjs/query/relations.js +1698 -0
- package/dist/cjs/query/where-compile.js +180 -0
- package/dist/cjs/query/where.js +1491 -0
- package/dist/cjs/query/writes.js +680 -0
- package/dist/cjs/schema-builder.js +6 -0
- package/dist/cjs/schema-metadata.js +4 -0
- package/dist/cjs/schema-sql.js +265 -3
- package/dist/cjs/sqlite.js +4 -1
- package/dist/cli/index.d.ts +8 -2
- package/dist/cli/index.js +111 -18
- package/dist/cli/migrate.d.ts +24 -1
- package/dist/cli/migrate.js +77 -3
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +46 -13
- package/dist/cli/studio.js +331 -23
- package/dist/cli/ui.js +7 -1
- package/dist/client.d.ts +32 -5
- package/dist/client.js +26 -4
- package/dist/dialect.d.ts +28 -6
- package/dist/dialect.js +2 -1
- package/dist/errors.d.ts +36 -0
- package/dist/errors.js +39 -0
- package/dist/generate.js +23 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/mssql.js +27 -5
- package/dist/mysql.js +4 -0
- package/dist/powdb.d.ts +135 -9
- package/dist/powdb.js +197 -25
- package/dist/powql.d.ts +166 -4
- package/dist/powql.js +516 -52
- package/dist/query/aggregates.d.ts +74 -0
- package/dist/query/aggregates.js +641 -0
- package/dist/query/batched-loader.d.ts +6 -0
- package/dist/query/batched-loader.js +2 -0
- package/dist/query/builder.d.ts +98 -830
- package/dist/query/builder.js +366 -4513
- package/dist/query/deferred.d.ts +13 -2
- package/dist/query/filters.d.ts +7 -0
- package/dist/query/filters.js +11 -0
- package/dist/query/relations.d.ts +441 -0
- package/dist/query/relations.js +1627 -0
- package/dist/query/types.d.ts +25 -6
- package/dist/query/where-compile.d.ts +139 -0
- package/dist/query/where-compile.js +175 -0
- package/dist/query/where.d.ts +494 -0
- package/dist/query/where.js +1431 -0
- package/dist/query/writes.d.ts +131 -0
- package/dist/query/writes.js +626 -0
- package/dist/schema-builder.d.ts +18 -3
- package/dist/schema-builder.js +6 -0
- package/dist/schema-metadata.js +4 -0
- package/dist/schema-sql.d.ts +60 -3
- package/dist/schema-sql.js +261 -4
- package/dist/schema.d.ts +10 -0
- package/dist/sqlite.js +4 -1
- package/package.json +4 -4
package/dist/query/types.d.ts
CHANGED
|
@@ -7,15 +7,19 @@ export type OrderDirection = 'asc' | 'desc';
|
|
|
7
7
|
/**
|
|
8
8
|
* How a query resolves its `with` relations.
|
|
9
9
|
*
|
|
10
|
-
* - `'join'
|
|
11
|
-
* `json_agg(json_build_object(...))` subqueries. One round-trip
|
|
12
|
-
* seek per parent row when the child FK is indexed.
|
|
13
|
-
*
|
|
10
|
+
* - `'join'`: one SQL statement with correlated
|
|
11
|
+
* `json_agg(json_build_object(...))` subqueries. One round-trip, an index
|
|
12
|
+
* seek per parent row when the child FK is indexed. On PowDB, `'join'`
|
|
13
|
+
* instead opts into native PowQL server-side joins where eligible.
|
|
14
|
+
* - `'batched'`: run the base query, then ONE flat follow-up query per
|
|
14
15
|
* relation (`WHERE fk = ANY($1)`), stitching children client-side. D levels
|
|
15
16
|
* cost D extra round-trips, but each is a single key-set lookup and rows come
|
|
16
|
-
* back flat
|
|
17
|
+
* back flat (a win when FK columns are unindexed or result sets are huge).
|
|
17
18
|
*
|
|
18
|
-
* Precedence: per-query arg > client `relationLoadStrategy` config >
|
|
19
|
+
* Precedence: per-query arg > client `relationLoadStrategy` config > the engine
|
|
20
|
+
* default. On SQL engines the default is `'join'`; on PowDB the default is the
|
|
21
|
+
* batched loaders (an ineligible relation falls back to them per-relation and
|
|
22
|
+
* silently even when `'join'` is requested).
|
|
19
23
|
*/
|
|
20
24
|
export type RelationLoadStrategy = 'join' | 'batched';
|
|
21
25
|
/**
|
|
@@ -317,6 +321,8 @@ export interface FindUniqueArgs<T, R extends object = {}, W extends TypedWithCla
|
|
|
317
321
|
relationLoadStrategy?: RelationLoadStrategy;
|
|
318
322
|
/** Opt out of configured {@link GlobalFilters}. See {@link SkipGlobalFilters}. */
|
|
319
323
|
skipGlobalFilters?: SkipGlobalFilters;
|
|
324
|
+
/** Include PII-tagged columns in the result. See {@link FindManyArgs.includePii}. */
|
|
325
|
+
includePii?: boolean;
|
|
320
326
|
}
|
|
321
327
|
export interface FindManyArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined> {
|
|
322
328
|
where?: WhereClause<T>;
|
|
@@ -345,6 +351,19 @@ export interface FindManyArgs<T, R extends object = {}, W extends TypedWithClaus
|
|
|
345
351
|
* force the warning even when it is disabled in config.
|
|
346
352
|
*/
|
|
347
353
|
warnOnUnlimited?: boolean;
|
|
354
|
+
/**
|
|
355
|
+
* Include PII-tagged columns (`defineSchema` `pii: true`) in the result.
|
|
356
|
+
*
|
|
357
|
+
* PII columns are EXCLUDED from default projections: they come back only when
|
|
358
|
+
* explicitly named in `select`, or when this flag is `true`. Set it to `true`
|
|
359
|
+
* to return every PII column at the top level AND at every nested `with`
|
|
360
|
+
* level of this query. Default `false`. Schemas with no PII-tagged columns are
|
|
361
|
+
* unaffected (the emitted SQL is byte-identical either way).
|
|
362
|
+
*
|
|
363
|
+
* Referencing a PII column in `where` / `orderBy` / `groupBy` / aggregates is
|
|
364
|
+
* always allowed regardless of this flag (the reference is explicit).
|
|
365
|
+
*/
|
|
366
|
+
includePii?: boolean;
|
|
348
367
|
}
|
|
349
368
|
export interface FindManyStreamArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined> extends FindManyArgs<T, R, W, S, O> {
|
|
350
369
|
/**
|
|
@@ -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;
|
|
@@ -0,0 +1,175 @@
|
|
|
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 { findArrayUniqueKey, findJsonUniqueKey, fingerprintArrayFilterShape, fingerprintJsonFilterShape, fingerprintOperatorShape, isArrayFilter, isJsonFilter, isTextSearchFilter, isUnmatchedPlainObject, isVectorFilter, isWhereOperator, sortedKeys, VECTOR_DISTANCE_COMPARATORS, } from './filters.js';
|
|
37
|
+
/** True when a normalized relation filter carries at least one cardinality key. */
|
|
38
|
+
function isRelationFilterObj(filterObj) {
|
|
39
|
+
return ('some' in filterObj || 'every' in filterObj || 'none' in filterObj || 'is' in filterObj || 'isNot' in filterObj);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* THE canonical WHERE enumeration. Yields events in sorted-key order (skipping
|
|
43
|
+
* `undefined`), dispatching combinators and relation filters, so every consumer
|
|
44
|
+
* (fingerprint, SQL build, param collect) walks identically.
|
|
45
|
+
*
|
|
46
|
+
* Combinator SKIP rules match the historical code exactly: an `OR`/`AND` whose
|
|
47
|
+
* value is a non-array or an empty array is skipped entirely (it contributes
|
|
48
|
+
* neither SQL, params, nor a fingerprint token); `NOT` is always emitted.
|
|
49
|
+
* A key that names a relation but whose value is not a `{ some/every/none/is/
|
|
50
|
+
* isNot }` filter falls through to the scalar path, exactly as before.
|
|
51
|
+
*/
|
|
52
|
+
export function walkWhere(host, where) {
|
|
53
|
+
const events = [];
|
|
54
|
+
for (const key of sortedKeys(where)) {
|
|
55
|
+
const value = where[key];
|
|
56
|
+
if (value === undefined)
|
|
57
|
+
continue;
|
|
58
|
+
if (key === 'OR') {
|
|
59
|
+
const arr = value;
|
|
60
|
+
if (!Array.isArray(arr) || arr.length === 0)
|
|
61
|
+
continue;
|
|
62
|
+
events.push({ kind: 'or', conditions: arr });
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (key === 'AND') {
|
|
66
|
+
const arr = value;
|
|
67
|
+
if (!Array.isArray(arr) || arr.length === 0)
|
|
68
|
+
continue;
|
|
69
|
+
events.push({ kind: 'and', conditions: arr });
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (key === 'NOT') {
|
|
73
|
+
events.push({ kind: 'not', condition: value });
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const relDef = host.tableMeta.relations[key];
|
|
77
|
+
if (relDef && typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
78
|
+
const filterObj = host.normalizeRelationFilter(relDef, value);
|
|
79
|
+
if (isRelationFilterObj(filterObj)) {
|
|
80
|
+
events.push({ kind: 'relation', key, relDef, filterObj });
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
events.push({ kind: 'scalar', key, value });
|
|
85
|
+
}
|
|
86
|
+
return events;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Column-aware SQL classification of a scalar WHERE value. Reproduces the SQL
|
|
90
|
+
* builder's linear fall-through: a JSON/array-shaped value on a non-JSON/array
|
|
91
|
+
* column falls THROUGH to the next shape (and ultimately equality) unless it
|
|
92
|
+
* carries a shape-unique key, in which case the build path reports a typed
|
|
93
|
+
* error (`*Throw`). Both the build and collect paths call this, so they can
|
|
94
|
+
* never classify the same value differently.
|
|
95
|
+
*/
|
|
96
|
+
export function classifyScalarForSql(host, rawColumn, value) {
|
|
97
|
+
if (value === null)
|
|
98
|
+
return { kind: 'null' };
|
|
99
|
+
if (typeof value === 'object' && !Array.isArray(value) && isVectorFilter(value)) {
|
|
100
|
+
return { kind: 'vector' };
|
|
101
|
+
}
|
|
102
|
+
if (typeof value === 'object' && !Array.isArray(value) && isJsonFilter(value)) {
|
|
103
|
+
const colType = host.getColumnPgType(rawColumn);
|
|
104
|
+
if (host.isJsonColumnType(colType))
|
|
105
|
+
return { kind: 'json' };
|
|
106
|
+
const jsonKey = findJsonUniqueKey(value);
|
|
107
|
+
if (jsonKey)
|
|
108
|
+
return { kind: 'jsonThrow', jsonKey };
|
|
109
|
+
// else: fall through, `equals`/`contains` on a non-JSON column keep their
|
|
110
|
+
// WhereOperator meaning (matches builder.ts: no `continue`).
|
|
111
|
+
}
|
|
112
|
+
if (typeof value === 'object' && !Array.isArray(value) && isArrayFilter(value)) {
|
|
113
|
+
const colType = host.getColumnPgType(rawColumn);
|
|
114
|
+
if (colType.startsWith('_'))
|
|
115
|
+
return { kind: 'array', colType };
|
|
116
|
+
const arrayKey = findArrayUniqueKey(value);
|
|
117
|
+
if (arrayKey)
|
|
118
|
+
return { kind: 'arrayThrow', arrayKey };
|
|
119
|
+
// else: fall through.
|
|
120
|
+
}
|
|
121
|
+
if (typeof value === 'object' && !Array.isArray(value) && isTextSearchFilter(value)) {
|
|
122
|
+
return { kind: 'textsearch' };
|
|
123
|
+
}
|
|
124
|
+
if (isWhereOperator(value))
|
|
125
|
+
return { kind: 'operator' };
|
|
126
|
+
return { kind: 'equality' };
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The fingerprint's scalar token: deliberately COLUMN-BLIND and in the
|
|
130
|
+
* historical fingerprint precedence (operator before vector/json/array), so a
|
|
131
|
+
* value that both looks like an operator and a JSON filter (`equals`/`contains`
|
|
132
|
+
* overlap) tokenizes as an operator exactly as it did before. Column-blindness
|
|
133
|
+
* only ever over-distinguishes versus {@link classifyScalarForSql}, which is
|
|
134
|
+
* safe: it can cause an extra cache MISS, never a wrong-value HIT.
|
|
135
|
+
*/
|
|
136
|
+
export function fingerprintScalarToken(value) {
|
|
137
|
+
// null → distinct from any value token.
|
|
138
|
+
if (value === null)
|
|
139
|
+
return 'null';
|
|
140
|
+
// Operator objects, checked first (column-blind precedence).
|
|
141
|
+
if (isWhereOperator(value))
|
|
142
|
+
return fingerprintOperatorShape(value);
|
|
143
|
+
// Vector distance filter: metric (operator) and present comparators change
|
|
144
|
+
// the SQL shape, so both go in the token.
|
|
145
|
+
if (typeof value === 'object' && !Array.isArray(value) && isVectorFilter(value)) {
|
|
146
|
+
const dist = value.distance;
|
|
147
|
+
const cmps = Object.keys(VECTOR_DISTANCE_COMPARATORS)
|
|
148
|
+
.filter((c) => dist[c] !== undefined)
|
|
149
|
+
.sort()
|
|
150
|
+
.join('|');
|
|
151
|
+
return `vec(${dist.metric},${cmps})`;
|
|
152
|
+
}
|
|
153
|
+
// JSON filter: range ops carry a numeric/string annotation (different cast).
|
|
154
|
+
if (typeof value === 'object' && !Array.isArray(value) && isJsonFilter(value)) {
|
|
155
|
+
return fingerprintJsonFilterShape(value);
|
|
156
|
+
}
|
|
157
|
+
// Array filter.
|
|
158
|
+
if (typeof value === 'object' && !Array.isArray(value) && isArrayFilter(value)) {
|
|
159
|
+
return `arr(${fingerprintArrayFilterShape(value)})`;
|
|
160
|
+
}
|
|
161
|
+
// Text search filter.
|
|
162
|
+
if (typeof value === 'object' && !Array.isArray(value) && isTextSearchFilter(value)) {
|
|
163
|
+
const cfg = value.config ?? 'english';
|
|
164
|
+
return `fts(${cfg})`;
|
|
165
|
+
}
|
|
166
|
+
// Plain object literal that matched no filter shape: a token distinct from
|
|
167
|
+
// real equality so a cache entry warmed by genuine equality can't serve it.
|
|
168
|
+
if (isUnmatchedPlainObject(value)) {
|
|
169
|
+
return `obj(${Object.keys(value)
|
|
170
|
+
.sort()
|
|
171
|
+
.join(',')})`;
|
|
172
|
+
}
|
|
173
|
+
// Plain equality.
|
|
174
|
+
return 'eq';
|
|
175
|
+
}
|