turbine-orm 0.50.0 → 0.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -66
- package/dist/adapters/cockroachdb.d.ts +5 -5
- package/dist/adapters/cockroachdb.js +10 -10
- package/dist/adapters/index.d.ts +5 -5
- package/dist/adapters/index.js +7 -7
- package/dist/adapters/yugabytedb.d.ts +7 -7
- package/dist/adapters/yugabytedb.js +10 -10
- package/dist/cjs/adapters/cockroachdb.d.ts +5 -5
- package/dist/cjs/adapters/cockroachdb.js +10 -10
- package/dist/cjs/adapters/index.d.ts +5 -5
- package/dist/cjs/adapters/index.js +7 -7
- package/dist/cjs/adapters/yugabytedb.d.ts +7 -7
- package/dist/cjs/adapters/yugabytedb.js +10 -10
- package/dist/cjs/cli/config.d.ts +13 -2
- package/dist/cjs/cli/config.js +3 -2
- package/dist/cjs/cli/destructive.d.ts +1 -1
- package/dist/cjs/cli/destructive.js +1 -1
- package/dist/cjs/cli/index.d.ts +10 -10
- package/dist/cjs/cli/index.js +49 -45
- package/dist/cjs/cli/loader.d.ts +7 -7
- package/dist/cjs/cli/loader.js +9 -9
- package/dist/cjs/cli/mcp.js +4 -4
- package/dist/cjs/cli/migrate.d.ts +5 -5
- package/dist/cjs/cli/migrate.js +11 -11
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/ui.d.ts +2 -2
- package/dist/cjs/cli/ui.js +2 -2
- package/dist/cjs/client.d.ts +49 -38
- package/dist/cjs/client.js +57 -56
- package/dist/cjs/dialect.d.ts +62 -18
- package/dist/cjs/dialect.js +40 -2
- package/dist/cjs/errors.d.ts +5 -5
- package/dist/cjs/errors.js +11 -11
- package/dist/cjs/generate.d.ts +6 -6
- package/dist/cjs/generate.js +31 -29
- package/dist/cjs/index-advisor.d.ts +5 -5
- package/dist/cjs/index-advisor.js +0 -0
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js +7 -7
- package/dist/cjs/introspect.d.ts +35 -9
- package/dist/cjs/introspect.js +83 -32
- package/dist/cjs/mssql.d.ts +11 -11
- package/dist/cjs/mssql.js +64 -29
- package/dist/cjs/mysql.d.ts +8 -8
- package/dist/cjs/mysql.js +61 -23
- package/dist/cjs/nested-write.d.ts +21 -2
- package/dist/cjs/nested-write.js +51 -14
- package/dist/cjs/optional-peer-import.cjs +7 -7
- package/dist/cjs/optional-peer-import.d.cts +7 -7
- package/dist/cjs/pipeline-submittable.d.ts +2 -2
- package/dist/cjs/pipeline-submittable.js +6 -6
- package/dist/cjs/pipeline.d.ts +1 -1
- package/dist/cjs/pipeline.js +4 -4
- package/dist/cjs/powdb-introspect.d.ts +1 -1
- package/dist/cjs/powdb-introspect.js +1 -1
- package/dist/cjs/powdb.d.ts +28 -28
- package/dist/cjs/powdb.js +66 -66
- package/dist/cjs/powql.d.ts +27 -27
- package/dist/cjs/powql.js +73 -52
- package/dist/cjs/query/aggregates.d.ts +1 -1
- package/dist/cjs/query/aggregates.js +5 -5
- package/dist/cjs/query/batched-loader.d.ts +11 -11
- package/dist/cjs/query/batched-loader.js +24 -24
- package/dist/cjs/query/builder.d.ts +39 -21
- package/dist/cjs/query/builder.js +99 -57
- package/dist/cjs/query/compound-unique.d.ts +1 -1
- package/dist/cjs/query/compound-unique.js +0 -0
- package/dist/cjs/query/deferred.d.ts +12 -6
- package/dist/cjs/query/deferred.js +1 -1
- package/dist/cjs/query/filters.d.ts +31 -11
- package/dist/cjs/query/filters.js +67 -14
- package/dist/cjs/query/index.d.ts +1 -1
- package/dist/cjs/query/index.js +1 -1
- package/dist/cjs/query/relations.d.ts +9 -9
- package/dist/cjs/query/relations.js +164 -57
- package/dist/cjs/query/types.d.ts +86 -35
- package/dist/cjs/query/types.js +1 -1
- package/dist/cjs/query/utils.d.ts +27 -10
- package/dist/cjs/query/utils.js +86 -14
- package/dist/cjs/query/where.d.ts +47 -28
- package/dist/cjs/query/where.js +130 -31
- package/dist/cjs/query/writes.d.ts +24 -5
- package/dist/cjs/query/writes.js +102 -13
- package/dist/cjs/realtime.d.ts +7 -7
- package/dist/cjs/realtime.js +9 -9
- package/dist/cjs/schema-builder.d.ts +18 -7
- package/dist/cjs/schema-builder.js +17 -10
- package/dist/cjs/schema-metadata.d.ts +3 -3
- package/dist/cjs/schema-metadata.js +9 -9
- package/dist/cjs/schema-sql.d.ts +9 -9
- package/dist/cjs/schema-sql.js +20 -20
- package/dist/cjs/schema.d.ts +19 -9
- package/dist/cjs/schema.js +6 -6
- package/dist/cjs/serverless.d.ts +15 -15
- package/dist/cjs/serverless.js +16 -16
- package/dist/cjs/sqlite.d.ts +8 -8
- package/dist/cjs/sqlite.js +53 -22
- package/dist/cjs/typed-sql.d.ts +4 -4
- package/dist/cjs/typed-sql.js +5 -5
- package/dist/cli/config.d.ts +13 -2
- package/dist/cli/config.js +3 -2
- package/dist/cli/destructive.d.ts +1 -1
- package/dist/cli/destructive.js +1 -1
- package/dist/cli/index.d.ts +10 -10
- package/dist/cli/index.js +49 -45
- package/dist/cli/loader.d.ts +7 -7
- package/dist/cli/loader.js +9 -9
- package/dist/cli/mcp.js +4 -4
- package/dist/cli/migrate.d.ts +5 -5
- package/dist/cli/migrate.js +11 -11
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/ui.d.ts +2 -2
- package/dist/cli/ui.js +2 -2
- package/dist/client.d.ts +49 -38
- package/dist/client.js +57 -56
- package/dist/dialect.d.ts +62 -18
- package/dist/dialect.js +40 -2
- package/dist/errors.d.ts +5 -5
- package/dist/errors.js +11 -11
- package/dist/generate.d.ts +6 -6
- package/dist/generate.js +31 -29
- package/dist/index-advisor.d.ts +5 -5
- package/dist/index-advisor.js +0 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -7
- package/dist/introspect.d.ts +35 -9
- package/dist/introspect.js +82 -32
- package/dist/mssql.d.ts +11 -11
- package/dist/mssql.js +64 -29
- package/dist/mysql.d.ts +8 -8
- package/dist/mysql.js +61 -23
- package/dist/nested-write.d.ts +21 -2
- package/dist/nested-write.js +51 -14
- package/dist/optional-peer-import.cjs +7 -7
- package/dist/optional-peer-import.d.cts +7 -7
- package/dist/pipeline-submittable.d.ts +2 -2
- package/dist/pipeline-submittable.js +6 -6
- package/dist/pipeline.d.ts +1 -1
- package/dist/pipeline.js +4 -4
- package/dist/powdb-introspect.d.ts +1 -1
- package/dist/powdb-introspect.js +1 -1
- package/dist/powdb.d.ts +28 -28
- package/dist/powdb.js +66 -66
- package/dist/powql.d.ts +27 -27
- package/dist/powql.js +73 -52
- package/dist/query/aggregates.d.ts +1 -1
- package/dist/query/aggregates.js +5 -5
- package/dist/query/batched-loader.d.ts +11 -11
- package/dist/query/batched-loader.js +24 -24
- package/dist/query/builder.d.ts +39 -21
- package/dist/query/builder.js +100 -58
- package/dist/query/compound-unique.d.ts +1 -1
- package/dist/query/compound-unique.js +0 -0
- package/dist/query/deferred.d.ts +12 -6
- package/dist/query/deferred.js +1 -1
- package/dist/query/filters.d.ts +31 -11
- package/dist/query/filters.js +66 -13
- package/dist/query/index.d.ts +1 -1
- package/dist/query/index.js +1 -1
- package/dist/query/relations.d.ts +9 -9
- package/dist/query/relations.js +165 -58
- package/dist/query/types.d.ts +86 -35
- package/dist/query/types.js +1 -1
- package/dist/query/utils.d.ts +27 -10
- package/dist/query/utils.js +84 -14
- package/dist/query/where.d.ts +47 -28
- package/dist/query/where.js +129 -32
- package/dist/query/writes.d.ts +24 -5
- package/dist/query/writes.js +101 -13
- package/dist/realtime.d.ts +7 -7
- package/dist/realtime.js +9 -9
- package/dist/schema-builder.d.ts +18 -7
- package/dist/schema-builder.js +17 -10
- package/dist/schema-metadata.d.ts +3 -3
- package/dist/schema-metadata.js +9 -9
- package/dist/schema-sql.d.ts +9 -9
- package/dist/schema-sql.js +20 -20
- package/dist/schema.d.ts +19 -9
- package/dist/schema.js +6 -6
- package/dist/serverless.d.ts +15 -15
- package/dist/serverless.js +16 -16
- package/dist/sqlite.d.ts +8 -8
- package/dist/sqlite.js +53 -22
- package/dist/typed-sql.d.ts +4 -4
- package/dist/typed-sql.js +5 -5
- package/package.json +2 -2
package/dist/query/where.d.ts
CHANGED
|
@@ -94,7 +94,7 @@ interface ColumnRefContext {
|
|
|
94
94
|
* The operator's own RAW (unquoted) column name. The `column` argument the
|
|
95
95
|
* operator builders receive is already quoted on the build side and raw on
|
|
96
96
|
* the collect side, so the temporal bind rewrite resolves the column's type
|
|
97
|
-
* from here instead
|
|
97
|
+
* from here instead, the one value both sides pass identically.
|
|
98
98
|
*/
|
|
99
99
|
rawColumn: string;
|
|
100
100
|
}
|
|
@@ -105,7 +105,7 @@ interface ColumnRefContext {
|
|
|
105
105
|
* `with`-clause `where` filters (against a per-subquery alias, `t0.col`) compile
|
|
106
106
|
* an arbitrary target table's where against a column qualifier. They differ ONLY
|
|
107
107
|
* in that qualifier, the correlation parent handed to `buildRelationFilter`, and
|
|
108
|
-
* the unknown-column error wording
|
|
108
|
+
* the unknown-column error wording, so a single scoped build/collect/fingerprint
|
|
109
109
|
* trio, driven by the SAME canonical {@link walkWhere} the top level uses, serves
|
|
110
110
|
* both. See `buildScopedWhere` / `collectScopedWhereParams` / `fingerprintScopedWhere`.
|
|
111
111
|
*/
|
|
@@ -114,7 +114,7 @@ interface WhereScope {
|
|
|
114
114
|
meta: TableMetadata;
|
|
115
115
|
/** The target table name (used for host binding + error messages). */
|
|
116
116
|
table: string;
|
|
117
|
-
/** SQL prefix before `q(col)
|
|
117
|
+
/** SQL prefix before `q(col)`, `"target".` for EXISTS sub-wheres, `t0.` for aliases. */
|
|
118
118
|
qualifier: string;
|
|
119
119
|
/** The `parentTable` correlation argument for nested `buildRelationFilter` calls. */
|
|
120
120
|
relationParent: string;
|
|
@@ -168,7 +168,7 @@ export declare function collectScalarParams(qi: BuilderCtx, key: string, value:
|
|
|
168
168
|
* Param-collect mirror of {@link buildRelationFilter} for one relation-filter
|
|
169
169
|
* object (`{ some/every/none/is/isNot }`, already normalized). Pushes, per
|
|
170
170
|
* present branch and in the canonical order some→none→every→is→isNot, the
|
|
171
|
-
* branch's sub-where params THEN the target table's global-filter params
|
|
171
|
+
* branch's sub-where params THEN the target table's global-filter params -
|
|
172
172
|
* exactly the order buildRelationFilter emits. When no global filter applies
|
|
173
173
|
* the gf calls are no-ops, so this stays byte-identical to the pre-0.28 path.
|
|
174
174
|
* Shared by every collect site that mirrors buildRelationFilter
|
|
@@ -226,7 +226,7 @@ export declare function targetGlobalFilterAlias(qi: BuilderCtx, targetTable: str
|
|
|
226
226
|
export declare function collectTargetGlobalFilterAlias(qi: BuilderCtx, targetTable: string, params: unknown[]): void;
|
|
227
227
|
/**
|
|
228
228
|
* SQL clause for `targetTable`'s global filter rendered against the bare
|
|
229
|
-
* (unaliased) table name
|
|
229
|
+
* (unaliased) table name, the form used inside relation-filter `EXISTS`
|
|
230
230
|
* subqueries. Pushes its params; `''` when none. Mirror:
|
|
231
231
|
* {@link collectTargetGlobalFilterExists}.
|
|
232
232
|
*/
|
|
@@ -247,7 +247,7 @@ export declare function globalFilterCacheSegment(qi: BuilderCtx): string;
|
|
|
247
247
|
/**
|
|
248
248
|
* True when the USER-supplied `where` compiles to no predicate (`{}`,
|
|
249
249
|
* `{ id: undefined }`, `{ OR: [{ a: undefined }] }`, …). This is the exact
|
|
250
|
-
* signal the empty-`where` guard needs
|
|
250
|
+
* signal the empty-`where` guard needs, the compiled emptiness, NOT the
|
|
251
251
|
* fingerprint (which is non-empty for an all-undefined `OR`/`AND`). It ignores
|
|
252
252
|
* any configured global filter, so a global filter never lets an unguarded
|
|
253
253
|
* mass mutation through.
|
|
@@ -269,7 +269,7 @@ export declare function buildWhereClause(qi: BuilderCtx, where: Record<string, u
|
|
|
269
269
|
*/
|
|
270
270
|
export declare function buildScalarClause(qi: BuilderCtx, key: string, value: unknown, params: unknown[], andClauses: string[]): void;
|
|
271
271
|
/**
|
|
272
|
-
* A {@link WhereHost} with no relations
|
|
272
|
+
* A {@link WhereHost} with no relations, used to fingerprint a sub-where
|
|
273
273
|
* whose target table is unknown (`schema.tables[t]` miss). `walkWhere` reads
|
|
274
274
|
* only `tableMeta.relations`, so every key falls to the scalar path, matching
|
|
275
275
|
* the pre-unification `meta?.relations` short-circuit.
|
|
@@ -283,7 +283,7 @@ export declare function aliasWhereScope(qi: BuilderCtx, targetTable: string, met
|
|
|
283
283
|
/**
|
|
284
284
|
* Compile a scoped sub-where to SQL. Serves BOTH the relation-filter EXISTS
|
|
285
285
|
* body ({@link buildSubWhereForRelation}) and the relation `with`-clause
|
|
286
|
-
* `where` ({@link buildAliasWhere})
|
|
286
|
+
* `where` ({@link buildAliasWhere}), the emitted SQL is byte-identical to the
|
|
287
287
|
* former hand-mirrored walkers, since it renders the same clauses in the same
|
|
288
288
|
* ({@link walkWhere}-canonical) key order.
|
|
289
289
|
*/
|
|
@@ -292,8 +292,8 @@ export declare function buildScopedWhere(qi: BuilderCtx, scope: WhereScope, wher
|
|
|
292
292
|
* Emit the SQL clause(s) for one scalar key of a scoped sub-where. Reproduces
|
|
293
293
|
* the null / JSON / array / operator / equality fall-through both former
|
|
294
294
|
* walkers shared (relation sub-wheres and alias wheres carry no vector or
|
|
295
|
-
* text-search scalar surface, so
|
|
296
|
-
*
|
|
295
|
+
* text-search scalar surface, so, unlike the top-level {@link buildScalarClause}
|
|
296
|
+
* - those shapes are not special-cased here and keep their historical
|
|
297
297
|
* equality-guard behavior).
|
|
298
298
|
*/
|
|
299
299
|
export declare function buildScopedScalarClause(qi: BuilderCtx, scope: WhereScope, field: string, value: unknown, params: unknown[], clauses: string[]): void;
|
|
@@ -340,7 +340,7 @@ export declare function pgTypeForColumn(_qi: BuilderCtx, meta: TableMetadata, co
|
|
|
340
340
|
* the UTC-component literal, so a predicate matches the value a write of the
|
|
341
341
|
* same `Date` stored.
|
|
342
342
|
*
|
|
343
|
-
* This is a VALUE transform only
|
|
343
|
+
* This is a VALUE transform only, it never changes the emitted SQL, so the
|
|
344
344
|
* SQL-template cache is unaffected, and it is applied on the cache-hit
|
|
345
345
|
* param-collect path as well as the build path.
|
|
346
346
|
*
|
|
@@ -353,13 +353,13 @@ export declare function coerceWhereOperand(qi: BuilderCtx, meta: TableMetadata,
|
|
|
353
353
|
* Introspection stores each column's `udt_name` in `pgTypes` and every
|
|
354
354
|
* database enum in `schema.enums` (typname → labels); a column whose type
|
|
355
355
|
* matches an enum key needs an explicit `::"EnumName"` cast on its write
|
|
356
|
-
* binds
|
|
356
|
+
* binds, bulk-insert forms like `UNNEST($1::text[])` otherwise type the
|
|
357
357
|
* value as text and Postgres refuses the implicit text→enum coercion
|
|
358
358
|
* ("column X is of type Y but expression is of type text").
|
|
359
359
|
*
|
|
360
360
|
* Postgres-only by construction: gated on the active dialect being
|
|
361
361
|
* `postgresql` AND on `schema.enums` having entries (only PG introspection
|
|
362
|
-
* produces them
|
|
362
|
+
* produces them, `defineSchema` and the other engines leave it empty), so
|
|
363
363
|
* SQLite/MySQL/MSSQL/PowDB output is byte-identical.
|
|
364
364
|
*/
|
|
365
365
|
export declare function enumTypeForColumn(qi: BuilderCtx, column: string): string | null;
|
|
@@ -382,7 +382,7 @@ export declare function assertBindableEqualityValue(qi: BuilderCtx, rawColumn: s
|
|
|
382
382
|
/**
|
|
383
383
|
* Build the user-supplied `where` filter of a relation `with` clause against
|
|
384
384
|
* the relation's table alias. Supports the same scalar surface as the
|
|
385
|
-
* top-level WHERE builder
|
|
385
|
+
* top-level WHERE builder, equality, IS NULL, operator objects (incl.
|
|
386
386
|
* `mode: 'insensitive'`), and OR/AND/NOT combinators. Unknown operator
|
|
387
387
|
* objects throw via {@link assertBindableEqualityValue}.
|
|
388
388
|
*
|
|
@@ -395,7 +395,7 @@ export declare function collectAliasWhereParams(qi: BuilderCtx, targetTable: str
|
|
|
395
395
|
/**
|
|
396
396
|
* Value-invariant, shape-aware fingerprint for a relation `with` clause's
|
|
397
397
|
* `where` filter. Must distinguish every SQL shape {@link buildAliasWhere}
|
|
398
|
-
* can emit
|
|
398
|
+
* can emit, equality vs null vs operator sets vs combinators, or two
|
|
399
399
|
* differently-shaped wheres would share one cached SQL string.
|
|
400
400
|
*/
|
|
401
401
|
export declare function fingerprintAliasWhere(qi: BuilderCtx, where: Record<string, unknown>, targetTable?: string): string;
|
|
@@ -444,7 +444,7 @@ export declare function requireArrayColumns(qi: BuilderCtx): void;
|
|
|
444
444
|
* Resolve a {@link VectorMetric} to its pgvector distance operator from a
|
|
445
445
|
* fixed allow-list, validating the target column is actually a `vector`
|
|
446
446
|
* column. Throws {@link ValidationError} for an unknown metric or a
|
|
447
|
-
* non-vector column
|
|
447
|
+
* non-vector column, a user-supplied string can never become a SQL operator.
|
|
448
448
|
*/
|
|
449
449
|
export declare function vectorOperator(qi: BuilderCtx, field: string, rawColumn: string, metric: string): string;
|
|
450
450
|
/**
|
|
@@ -456,8 +456,8 @@ export declare function vectorOperator(qi: BuilderCtx, field: string, rawColumn:
|
|
|
456
456
|
*/
|
|
457
457
|
export declare function pushVectorParam(qi: BuilderCtx, field: string, _rawColumn: string, to: unknown, params: unknown[]): string;
|
|
458
458
|
/**
|
|
459
|
-
* Prisma-compat: a plain object on a to-one relation key
|
|
460
|
-
* `where: { vendor: { name: { contains: 'x' } } }
|
|
459
|
+
* Prisma-compat: a plain object on a to-one relation key -
|
|
460
|
+
* `where: { vendor: { name: { contains: 'x' } } }`, is an implicit `is`
|
|
461
461
|
* filter. Normalize it to `{ is: obj }` so all downstream handling (SQL,
|
|
462
462
|
* params, fingerprint) sees one canonical shape. To-many relations still
|
|
463
463
|
* require an explicit `some`/`every`/`none` (a bare object there is
|
|
@@ -468,7 +468,7 @@ export declare function normalizeRelationFilter(_qi: BuilderCtx, relDef: Relatio
|
|
|
468
468
|
* Case-insensitive json/jsonb column-type check. Postgres reports lowercase
|
|
469
469
|
* udt_names, but SQLite/MySQL introspection surfaces the DECLARED type
|
|
470
470
|
* (e.g. `JSON`), so every JSON-feature gate compares through this predicate
|
|
471
|
-
*
|
|
471
|
+
* - build and collect sides alike, keeping the SQL-cache lockstep.
|
|
472
472
|
*/
|
|
473
473
|
export declare function isJsonColumnType(_qi: BuilderCtx, colType: string): boolean;
|
|
474
474
|
export declare function getColumnPgType(qi: BuilderCtx, column: string): string;
|
|
@@ -478,13 +478,32 @@ export declare function getColumnPgType(qi: BuilderCtx, column: string): string;
|
|
|
478
478
|
*/
|
|
479
479
|
export declare function getArrayElementType(_qi: BuilderCtx, pgType: string): string;
|
|
480
480
|
/**
|
|
481
|
-
*
|
|
482
|
-
*
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
*
|
|
486
|
-
*
|
|
481
|
+
* Refuse a {@link JsonFilter} carrying a key that is not a JSON operator, and
|
|
482
|
+
* refuse a filter that selects a `path` but never compares it.
|
|
483
|
+
*
|
|
484
|
+
* Both shapes used to compile to NOTHING. `buildJsonFilterClauses` only ever
|
|
485
|
+
* emitted a clause for a key it recognized, so `{ path: ['title'],
|
|
486
|
+
* string_contains: 'x' }`, the Prisma spelling, and an easy typo besides -
|
|
487
|
+
* produced an empty clause list, the predicate vanished, and the query
|
|
488
|
+
* returned EVERY row. Inside an `AND` it silently dropped that conjunct, so a
|
|
489
|
+
* tenant scope written this way widened to the whole table. The equivalent
|
|
490
|
+
* typo on a scalar column has always thrown; this closes the inconsistency.
|
|
491
|
+
*
|
|
492
|
+
* Called from both the SQL-build path and the cache-hit param-collect path, so
|
|
493
|
+
* a warmed SQL cache cannot skip the check.
|
|
487
494
|
*/
|
|
495
|
+
export declare function assertJsonFilterKeys(filter: JsonFilter, column: string): void;
|
|
496
|
+
/**
|
|
497
|
+
* Validate and enumerate the substring comparisons on a JSON filter, in the
|
|
498
|
+
* fixed {@link JSON_STRING_OPERATORS} order. Shared by the build and collect
|
|
499
|
+
* paths exactly like {@link jsonRangeEntries}, so both agree on the params
|
|
500
|
+
* pushed and both throw identically on an invalid shape.
|
|
501
|
+
*/
|
|
502
|
+
export declare function jsonStringEntries(filter: JsonFilter, column: string): {
|
|
503
|
+
op: string;
|
|
504
|
+
pattern: (escaped: string) => string;
|
|
505
|
+
value: string;
|
|
506
|
+
}[];
|
|
488
507
|
export declare function jsonRangeEntries(_qi: BuilderCtx, filter: JsonFilter, column: string): {
|
|
489
508
|
sqlOp: string;
|
|
490
509
|
value: number | string;
|
|
@@ -500,19 +519,19 @@ export declare function jsonRangeEntries(_qi: BuilderCtx, filter: JsonFilter, co
|
|
|
500
519
|
export declare function buildJsonFilterClauses(qi: BuilderCtx, column: string, filter: JsonFilter, params: unknown[]): string[];
|
|
501
520
|
/**
|
|
502
521
|
* Bind value for a JSON path parameter, encoded per dialect. PostgreSQL's
|
|
503
|
-
* `#>>` takes a `text[]` (the segments as strings
|
|
522
|
+
* `#>>` takes a `text[]` (the segments as strings, or `nativeForm` when the
|
|
504
523
|
* caller has a specific native binding, e.g. JsonFilter's raw path array).
|
|
505
524
|
* Every other engine's JSON function (`json_extract` / `JSON_EXTRACT` /
|
|
506
525
|
* `JSON_VALUE`) takes a `'$'`-rooted JSONPath STRING: binding the raw array
|
|
507
526
|
* would arrive as `'["a"]'` (the driver shims JSON.stringify non-primitive
|
|
508
527
|
* params) and fail at runtime with the engine's bad-JSON-path error. The
|
|
509
|
-
* encoded path stays a bound parameter
|
|
528
|
+
* encoded path stays a bound parameter, never spliced into SQL text, so
|
|
510
529
|
* the build/collect param mirrors stay in lockstep and injection-safe.
|
|
511
530
|
*/
|
|
512
531
|
export declare function jsonPathParam(qi: BuilderCtx, path: readonly (string | number)[], nativeForm?: unknown): unknown;
|
|
513
532
|
/**
|
|
514
533
|
* Cast an extracted JSON path text value to a numeric type for range
|
|
515
|
-
* comparison. PostgreSQL uses `(expr)::numeric` (exact
|
|
534
|
+
* comparison. PostgreSQL uses `(expr)::numeric` (exact, the right way to
|
|
516
535
|
* compare JSON numbers, and `::float` would lose precision on big ints);
|
|
517
536
|
* other dialects route through {@link Dialect.castAggregate} (SQLite/MySQL/
|
|
518
537
|
* SQL Server have no `::` operator) as a float cast.
|
package/dist/query/where.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { UnsupportedFeatureError, ValidationError } from '../errors.js';
|
|
14
14
|
import { camelToSnake, normalizeKeyColumns } from '../schema.js';
|
|
15
|
-
import { assertBindableEqualsOperand, findArrayUniqueKey, findJsonUniqueKey, isArrayFilter, isColumnRef, isJsonFilter, isUnmatchedPlainObject, isWhereOperator, JSON_RANGE_OPERATORS, VECTOR_DISTANCE_COMPARATORS, VECTOR_METRIC_OPERATORS, validateTextSearchConfig, } from './filters.js';
|
|
15
|
+
import { assertBindableEqualsOperand, findArrayUniqueKey, findJsonUniqueKey, isArrayFilter, isColumnRef, isJsonFilter, isUnmatchedPlainObject, isWhereOperator, JSON_FILTER_KEYS, JSON_RANGE_OPERATORS, JSON_STRING_OPERATORS, VECTOR_DISTANCE_COMPARATORS, VECTOR_METRIC_OPERATORS, validateTextSearchConfig, } from './filters.js';
|
|
16
16
|
import { coerceTemporalValue, escapeLike, OPERATOR_KEYS, ownLookup } from './utils.js';
|
|
17
17
|
import { classifyScalarForSql, fingerprintScalarToken, walkWhere, } from './where-compile.js';
|
|
18
18
|
/**
|
|
@@ -174,7 +174,7 @@ export function collectScalarParams(qi, key, value, params) {
|
|
|
174
174
|
* Param-collect mirror of {@link buildRelationFilter} for one relation-filter
|
|
175
175
|
* object (`{ some/every/none/is/isNot }`, already normalized). Pushes, per
|
|
176
176
|
* present branch and in the canonical order some→none→every→is→isNot, the
|
|
177
|
-
* branch's sub-where params THEN the target table's global-filter params
|
|
177
|
+
* branch's sub-where params THEN the target table's global-filter params -
|
|
178
178
|
* exactly the order buildRelationFilter emits. When no global filter applies
|
|
179
179
|
* the gf calls are no-ops, so this stays byte-identical to the pre-0.28 path.
|
|
180
180
|
* Shared by every collect site that mirrors buildRelationFilter
|
|
@@ -192,7 +192,7 @@ export function collectRelationFilterParams(qi, relDef, filterObj, params) {
|
|
|
192
192
|
}
|
|
193
193
|
if (filterObj.every !== undefined && filterObj.every !== null) {
|
|
194
194
|
// gf is only emitted (build) when the `every` sub-where compiles to a
|
|
195
|
-
// filter
|
|
195
|
+
// filter, otherwise `every` is trivially true and no subquery is built.
|
|
196
196
|
if (buildSubWhereForRelation(qi, target, filterObj.every, []) !== null) {
|
|
197
197
|
collectRelFilterParams(qi, target, filterObj.every, params);
|
|
198
198
|
collectTargetGlobalFilterExists(qi, target, params);
|
|
@@ -263,6 +263,7 @@ export function collectOperatorParams(qi, column, op, params, refCtx) {
|
|
|
263
263
|
* comparison values in {@link JSON_RANGE_OPERATORS} order.
|
|
264
264
|
*/
|
|
265
265
|
export function collectJsonFilterParams(qi, filter, params, column) {
|
|
266
|
+
assertJsonFilterKeys(filter, column);
|
|
266
267
|
let pathPushed = false;
|
|
267
268
|
const pushPathOnce = () => {
|
|
268
269
|
if (!pathPushed) {
|
|
@@ -288,6 +289,10 @@ export function collectJsonFilterParams(qi, filter, params, column) {
|
|
|
288
289
|
pushPathOnce();
|
|
289
290
|
params.push(value);
|
|
290
291
|
}
|
|
292
|
+
for (const { pattern, value } of jsonStringEntries(filter, column)) {
|
|
293
|
+
pushPathOnce();
|
|
294
|
+
params.push(pattern(escapeLike(value)));
|
|
295
|
+
}
|
|
291
296
|
}
|
|
292
297
|
/** Collect params from array filter. Mirrors buildArrayFilterClauses. */
|
|
293
298
|
export function collectArrayFilterParams(qi, filter, params) {
|
|
@@ -343,7 +348,7 @@ export function resolveGlobalFilter(qi, table, skip = qi.currentSkip) {
|
|
|
343
348
|
return null;
|
|
344
349
|
const obj = resolved;
|
|
345
350
|
// An all-undefined filter (e.g. `{ tenantId: undefined }`) contributes
|
|
346
|
-
// nothing
|
|
351
|
+
// nothing, treat it as absent so it never emits a dangling clause.
|
|
347
352
|
if (Object.keys(obj).every((k) => obj[k] === undefined))
|
|
348
353
|
return null;
|
|
349
354
|
return obj;
|
|
@@ -388,7 +393,7 @@ export function collectTargetGlobalFilterAlias(qi, targetTable, params) {
|
|
|
388
393
|
}
|
|
389
394
|
/**
|
|
390
395
|
* SQL clause for `targetTable`'s global filter rendered against the bare
|
|
391
|
-
* (unaliased) table name
|
|
396
|
+
* (unaliased) table name, the form used inside relation-filter `EXISTS`
|
|
392
397
|
* subqueries. Pushes its params; `''` when none. Mirror:
|
|
393
398
|
* {@link collectTargetGlobalFilterExists}.
|
|
394
399
|
*/
|
|
@@ -450,7 +455,7 @@ export function globalFilterCacheSegment(qi) {
|
|
|
450
455
|
/**
|
|
451
456
|
* True when the USER-supplied `where` compiles to no predicate (`{}`,
|
|
452
457
|
* `{ id: undefined }`, `{ OR: [{ a: undefined }] }`, …). This is the exact
|
|
453
|
-
* signal the empty-`where` guard needs
|
|
458
|
+
* signal the empty-`where` guard needs, the compiled emptiness, NOT the
|
|
454
459
|
* fingerprint (which is non-empty for an all-undefined `OR`/`AND`). It ignores
|
|
455
460
|
* any configured global filter, so a global filter never lets an unguarded
|
|
456
461
|
* mass mutation through.
|
|
@@ -572,7 +577,7 @@ export function buildScalarClause(qi, key, value, params, andClauses) {
|
|
|
572
577
|
}
|
|
573
578
|
}
|
|
574
579
|
/**
|
|
575
|
-
* A {@link WhereHost} with no relations
|
|
580
|
+
* A {@link WhereHost} with no relations, used to fingerprint a sub-where
|
|
576
581
|
* whose target table is unknown (`schema.tables[t]` miss). `walkWhere` reads
|
|
577
582
|
* only `tableMeta.relations`, so every key falls to the scalar path, matching
|
|
578
583
|
* the pre-unification `meta?.relations` short-circuit.
|
|
@@ -624,7 +629,7 @@ export function aliasWhereScope(qi, targetTable, meta, alias) {
|
|
|
624
629
|
/**
|
|
625
630
|
* Compile a scoped sub-where to SQL. Serves BOTH the relation-filter EXISTS
|
|
626
631
|
* body ({@link buildSubWhereForRelation}) and the relation `with`-clause
|
|
627
|
-
* `where` ({@link buildAliasWhere})
|
|
632
|
+
* `where` ({@link buildAliasWhere}), the emitted SQL is byte-identical to the
|
|
628
633
|
* former hand-mirrored walkers, since it renders the same clauses in the same
|
|
629
634
|
* ({@link walkWhere}-canonical) key order.
|
|
630
635
|
*/
|
|
@@ -665,8 +670,8 @@ export function buildScopedWhere(qi, scope, where, params) {
|
|
|
665
670
|
* Emit the SQL clause(s) for one scalar key of a scoped sub-where. Reproduces
|
|
666
671
|
* the null / JSON / array / operator / equality fall-through both former
|
|
667
672
|
* walkers shared (relation sub-wheres and alias wheres carry no vector or
|
|
668
|
-
* text-search scalar surface, so
|
|
669
|
-
*
|
|
673
|
+
* text-search scalar surface, so, unlike the top-level {@link buildScalarClause}
|
|
674
|
+
* - those shapes are not special-cased here and keep their historical
|
|
670
675
|
* equality-guard behavior).
|
|
671
676
|
*/
|
|
672
677
|
export function buildScopedScalarClause(qi, scope, field, value, params, clauses) {
|
|
@@ -866,7 +871,7 @@ export function buildRelationFilter(qi, _relName, relDef, filterObj, params, par
|
|
|
866
871
|
// DOMAIN of correlated rows in EVERY branch: `some`/`none`/`is`/`isNot`
|
|
867
872
|
// ignore filtered-out rows, and `every` quantifies over only the surviving
|
|
868
873
|
// rows ("every NON-deleted related row matches P"). It is ANDed into the
|
|
869
|
-
// correlation and its params pushed AFTER the per-branch filter
|
|
874
|
+
// correlation and its params pushed AFTER the per-branch filter, mirrored
|
|
870
875
|
// exactly in collectWhereParams' relation-filter branch. `qt` is the bare
|
|
871
876
|
// target table, matching the `FROM ${qt}` here (see targetGlobalFilterExists).
|
|
872
877
|
const gfAnd = () => {
|
|
@@ -901,10 +906,10 @@ export function buildRelationFilter(qi, _relName, relDef, filterObj, params, par
|
|
|
901
906
|
clauses.push(`NOT EXISTS (SELECT 1 FROM ${qt} WHERE ${correlation}${gf} AND NOT (${filterClause}))`);
|
|
902
907
|
}
|
|
903
908
|
else {
|
|
904
|
-
// "every" with empty filter = true (all match trivially)
|
|
909
|
+
// "every" with empty filter = true (all match trivially), gf irrelevant.
|
|
905
910
|
}
|
|
906
911
|
}
|
|
907
|
-
// "is": EXISTS
|
|
912
|
+
// "is": EXISTS, for to-one relations (same SQL as "some").
|
|
908
913
|
// `is: null` = "no related row" (Prisma semantics) → NOT EXISTS.
|
|
909
914
|
if (filterObj.is !== undefined) {
|
|
910
915
|
if (filterObj.is === null) {
|
|
@@ -917,7 +922,7 @@ export function buildRelationFilter(qi, _relName, relDef, filterObj, params, par
|
|
|
917
922
|
clauses.push(`EXISTS (SELECT 1 FROM ${qt} WHERE ${correlation}${filterAnd}${gfAnd()})`);
|
|
918
923
|
}
|
|
919
924
|
}
|
|
920
|
-
// "isNot": NOT EXISTS
|
|
925
|
+
// "isNot": NOT EXISTS, for to-one relations (same SQL as "none").
|
|
921
926
|
// `isNot: null` = "a related row exists" → EXISTS.
|
|
922
927
|
if (filterObj.isNot !== undefined) {
|
|
923
928
|
if (filterObj.isNot === null) {
|
|
@@ -958,7 +963,7 @@ export function pgTypeForColumn(_qi, meta, column) {
|
|
|
958
963
|
* the UTC-component literal, so a predicate matches the value a write of the
|
|
959
964
|
* same `Date` stored.
|
|
960
965
|
*
|
|
961
|
-
* This is a VALUE transform only
|
|
966
|
+
* This is a VALUE transform only, it never changes the emitted SQL, so the
|
|
962
967
|
* SQL-template cache is unaffected, and it is applied on the cache-hit
|
|
963
968
|
* param-collect path as well as the build path.
|
|
964
969
|
*
|
|
@@ -979,13 +984,13 @@ export function coerceWhereOperand(qi, meta, column, value) {
|
|
|
979
984
|
* Introspection stores each column's `udt_name` in `pgTypes` and every
|
|
980
985
|
* database enum in `schema.enums` (typname → labels); a column whose type
|
|
981
986
|
* matches an enum key needs an explicit `::"EnumName"` cast on its write
|
|
982
|
-
* binds
|
|
987
|
+
* binds, bulk-insert forms like `UNNEST($1::text[])` otherwise type the
|
|
983
988
|
* value as text and Postgres refuses the implicit text→enum coercion
|
|
984
989
|
* ("column X is of type Y but expression is of type text").
|
|
985
990
|
*
|
|
986
991
|
* Postgres-only by construction: gated on the active dialect being
|
|
987
992
|
* `postgresql` AND on `schema.enums` having entries (only PG introspection
|
|
988
|
-
* produces them
|
|
993
|
+
* produces them, `defineSchema` and the other engines leave it empty), so
|
|
989
994
|
* SQLite/MySQL/MSSQL/PowDB output is byte-identical.
|
|
990
995
|
*/
|
|
991
996
|
export function enumTypeForColumn(qi, column) {
|
|
@@ -996,7 +1001,7 @@ export function enumTypeForColumn(qi, column) {
|
|
|
996
1001
|
return null;
|
|
997
1002
|
// Cross-schema guard (N-5): introspection records pgTypeSchema ONLY when
|
|
998
1003
|
// the column's type lives OUTSIDE the introspected schema. A same-named
|
|
999
|
-
// enum in another schema must not get this schema's cast
|
|
1004
|
+
// enum in another schema must not get this schema's cast, search_path
|
|
1000
1005
|
// would resolve `::"status"` to the wrong type. Skipping the cast restores
|
|
1001
1006
|
// the pre-cast behavior for such columns. Columns without pgTypeSchema
|
|
1002
1007
|
// (same-schema types, defineSchema/legacy metadata) keep the cast.
|
|
@@ -1041,7 +1046,7 @@ export function assertBindableEqualityValue(qi, rawColumn, value, columnPgType,
|
|
|
1041
1046
|
/**
|
|
1042
1047
|
* Build the user-supplied `where` filter of a relation `with` clause against
|
|
1043
1048
|
* the relation's table alias. Supports the same scalar surface as the
|
|
1044
|
-
* top-level WHERE builder
|
|
1049
|
+
* top-level WHERE builder, equality, IS NULL, operator objects (incl.
|
|
1045
1050
|
* `mode: 'insensitive'`), and OR/AND/NOT combinators. Unknown operator
|
|
1046
1051
|
* objects throw via {@link assertBindableEqualityValue}.
|
|
1047
1052
|
*
|
|
@@ -1060,7 +1065,7 @@ export function collectAliasWhereParams(qi, targetTable, targetMeta, where, para
|
|
|
1060
1065
|
/**
|
|
1061
1066
|
* Value-invariant, shape-aware fingerprint for a relation `with` clause's
|
|
1062
1067
|
* `where` filter. Must distinguish every SQL shape {@link buildAliasWhere}
|
|
1063
|
-
* can emit
|
|
1068
|
+
* can emit, equality vs null vs operator sets vs combinators, or two
|
|
1064
1069
|
* differently-shaped wheres would share one cached SQL string.
|
|
1065
1070
|
*/
|
|
1066
1071
|
export function fingerprintAliasWhere(qi, where, targetTable) {
|
|
@@ -1182,18 +1187,18 @@ export function buildOperatorClauses(qi, column, op, params, refCtx) {
|
|
|
1182
1187
|
params.push(qi.inParam(cv(op.notIn)));
|
|
1183
1188
|
clauses.push(qi.inClause(column, qi.p(params.length), true));
|
|
1184
1189
|
}
|
|
1185
|
-
const
|
|
1190
|
+
const insensitive = op.mode === 'insensitive';
|
|
1186
1191
|
if (op.contains !== undefined) {
|
|
1187
1192
|
params.push(`%${escapeLike(op.contains)}%`);
|
|
1188
|
-
clauses.push(
|
|
1193
|
+
clauses.push(buildLikeClause(qi, column, qi.p(params.length), insensitive));
|
|
1189
1194
|
}
|
|
1190
1195
|
if (op.startsWith !== undefined) {
|
|
1191
1196
|
params.push(`${escapeLike(op.startsWith)}%`);
|
|
1192
|
-
clauses.push(
|
|
1197
|
+
clauses.push(buildLikeClause(qi, column, qi.p(params.length), insensitive));
|
|
1193
1198
|
}
|
|
1194
1199
|
if (op.endsWith !== undefined) {
|
|
1195
1200
|
params.push(`%${escapeLike(op.endsWith)}`);
|
|
1196
|
-
clauses.push(
|
|
1201
|
+
clauses.push(buildLikeClause(qi, column, qi.p(params.length), insensitive));
|
|
1197
1202
|
}
|
|
1198
1203
|
return clauses;
|
|
1199
1204
|
}
|
|
@@ -1228,7 +1233,7 @@ export function requireArrayColumns(qi) {
|
|
|
1228
1233
|
* Resolve a {@link VectorMetric} to its pgvector distance operator from a
|
|
1229
1234
|
* fixed allow-list, validating the target column is actually a `vector`
|
|
1230
1235
|
* column. Throws {@link ValidationError} for an unknown metric or a
|
|
1231
|
-
* non-vector column
|
|
1236
|
+
* non-vector column, a user-supplied string can never become a SQL operator.
|
|
1232
1237
|
*/
|
|
1233
1238
|
export function vectorOperator(qi, field, rawColumn, metric) {
|
|
1234
1239
|
if (!qi.dialect.supportsVector) {
|
|
@@ -1275,8 +1280,8 @@ export function pushVectorParam(qi, field, _rawColumn, to, params) {
|
|
|
1275
1280
|
return `${qi.p(params.length)}::vector`;
|
|
1276
1281
|
}
|
|
1277
1282
|
/**
|
|
1278
|
-
* Prisma-compat: a plain object on a to-one relation key
|
|
1279
|
-
* `where: { vendor: { name: { contains: 'x' } } }
|
|
1283
|
+
* Prisma-compat: a plain object on a to-one relation key -
|
|
1284
|
+
* `where: { vendor: { name: { contains: 'x' } } }`, is an implicit `is`
|
|
1280
1285
|
* filter. Normalize it to `{ is: obj }` so all downstream handling (SQL,
|
|
1281
1286
|
* params, fingerprint) sees one canonical shape. To-many relations still
|
|
1282
1287
|
* require an explicit `some`/`every`/`none` (a bare object there is
|
|
@@ -1297,7 +1302,7 @@ export function normalizeRelationFilter(_qi, relDef, filterObj) {
|
|
|
1297
1302
|
* Case-insensitive json/jsonb column-type check. Postgres reports lowercase
|
|
1298
1303
|
* udt_names, but SQLite/MySQL introspection surfaces the DECLARED type
|
|
1299
1304
|
* (e.g. `JSON`), so every JSON-feature gate compares through this predicate
|
|
1300
|
-
*
|
|
1305
|
+
* - build and collect sides alike, keeping the SQL-cache lockstep.
|
|
1301
1306
|
*/
|
|
1302
1307
|
export function isJsonColumnType(_qi, colType) {
|
|
1303
1308
|
const t = colType.toLowerCase();
|
|
@@ -1334,9 +1339,92 @@ export function getArrayElementType(_qi, pgType) {
|
|
|
1334
1339
|
* JSON filter, in the fixed {@link JSON_RANGE_OPERATORS} order. Shared by
|
|
1335
1340
|
* the SQL-build path ({@link buildJsonFilterClauses}) and the cache-hit
|
|
1336
1341
|
* param-collect path ({@link collectJsonFilterParams}) so both always agree
|
|
1337
|
-
* on which params are pushed
|
|
1342
|
+
* on which params are pushed, and both throw identically for invalid
|
|
1338
1343
|
* shapes, so a warmed cache can never skip validation.
|
|
1339
1344
|
*/
|
|
1345
|
+
/**
|
|
1346
|
+
* One LIKE comparison, honoring `mode: 'insensitive'` through the dialect and
|
|
1347
|
+
* always carrying the `ESCAPE '\'` clause that pairs with {@link escapeLike}.
|
|
1348
|
+
*
|
|
1349
|
+
* Shared by the scalar `contains` / `startsWith` / `endsWith` operators and by
|
|
1350
|
+
* the JSON substring operators, so the two can never drift into escaping their
|
|
1351
|
+
* operands the same way but comparing them differently.
|
|
1352
|
+
*/
|
|
1353
|
+
function buildLikeClause(qi, column, paramRef, insensitive) {
|
|
1354
|
+
const base = insensitive ? qi.dialect.buildInsensitiveLike(column, paramRef) : `${column} LIKE ${paramRef}`;
|
|
1355
|
+
return `${base} ESCAPE '\\'`;
|
|
1356
|
+
}
|
|
1357
|
+
/**
|
|
1358
|
+
* Refuse a {@link JsonFilter} carrying a key that is not a JSON operator, and
|
|
1359
|
+
* refuse a filter that selects a `path` but never compares it.
|
|
1360
|
+
*
|
|
1361
|
+
* Both shapes used to compile to NOTHING. `buildJsonFilterClauses` only ever
|
|
1362
|
+
* emitted a clause for a key it recognized, so `{ path: ['title'],
|
|
1363
|
+
* string_contains: 'x' }`, the Prisma spelling, and an easy typo besides -
|
|
1364
|
+
* produced an empty clause list, the predicate vanished, and the query
|
|
1365
|
+
* returned EVERY row. Inside an `AND` it silently dropped that conjunct, so a
|
|
1366
|
+
* tenant scope written this way widened to the whole table. The equivalent
|
|
1367
|
+
* typo on a scalar column has always thrown; this closes the inconsistency.
|
|
1368
|
+
*
|
|
1369
|
+
* Called from both the SQL-build path and the cache-hit param-collect path, so
|
|
1370
|
+
* a warmed SQL cache cannot skip the check.
|
|
1371
|
+
*/
|
|
1372
|
+
export function assertJsonFilterKeys(filter, column) {
|
|
1373
|
+
const obj = filter;
|
|
1374
|
+
const present = Object.keys(obj).filter((k) => obj[k] !== undefined);
|
|
1375
|
+
for (const key of present) {
|
|
1376
|
+
if (JSON_FILTER_KEYS.has(key))
|
|
1377
|
+
continue;
|
|
1378
|
+
const suggestion = JSON_PRISMA_SPELLINGS[key];
|
|
1379
|
+
throw new ValidationError(`[turbine] Unknown JSON filter operator "${key}" on ${column}.` +
|
|
1380
|
+
(suggestion ? ` Did you mean \`${suggestion}\`?` : '') +
|
|
1381
|
+
` Supported operators: ${[...JSON_FILTER_KEYS].sort().join(', ')}.`);
|
|
1382
|
+
}
|
|
1383
|
+
// `mode` and `path` are modifiers, not comparisons: a filter made only of
|
|
1384
|
+
// those compares nothing, which is exactly the silent no-op shape above.
|
|
1385
|
+
if (present.length > 0 && present.every((k) => k === 'path' || k === 'mode')) {
|
|
1386
|
+
throw new ValidationError(`[turbine] JSON filter on ${column} selects a \`path\` but has no comparison. ` +
|
|
1387
|
+
`Add one of: ${[...JSON_FILTER_KEYS]
|
|
1388
|
+
.filter((k) => k !== 'path' && k !== 'mode')
|
|
1389
|
+
.sort()
|
|
1390
|
+
.join(', ')}.`);
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* The Prisma spelling of each JSON operator, so a migrator who writes the
|
|
1395
|
+
* name they already know gets told the turbine one instead of a bare list.
|
|
1396
|
+
*/
|
|
1397
|
+
const JSON_PRISMA_SPELLINGS = {
|
|
1398
|
+
string_contains: 'stringContains',
|
|
1399
|
+
string_starts_with: 'stringStartsWith',
|
|
1400
|
+
string_ends_with: 'stringEndsWith',
|
|
1401
|
+
array_contains: 'contains',
|
|
1402
|
+
startsWith: 'stringStartsWith',
|
|
1403
|
+
endsWith: 'stringEndsWith',
|
|
1404
|
+
};
|
|
1405
|
+
/**
|
|
1406
|
+
* Validate and enumerate the substring comparisons on a JSON filter, in the
|
|
1407
|
+
* fixed {@link JSON_STRING_OPERATORS} order. Shared by the build and collect
|
|
1408
|
+
* paths exactly like {@link jsonRangeEntries}, so both agree on the params
|
|
1409
|
+
* pushed and both throw identically on an invalid shape.
|
|
1410
|
+
*/
|
|
1411
|
+
export function jsonStringEntries(filter, column) {
|
|
1412
|
+
const entries = [];
|
|
1413
|
+
for (const [op, pattern] of Object.entries(JSON_STRING_OPERATORS)) {
|
|
1414
|
+
const value = filter[op];
|
|
1415
|
+
if (value === undefined)
|
|
1416
|
+
continue;
|
|
1417
|
+
if (filter.path === undefined) {
|
|
1418
|
+
throw new ValidationError(`[turbine] JSON operator '${op}' on ${column} requires a \`path\` ` +
|
|
1419
|
+
`(e.g. { path: ['meta', 'title'], ${op}: ${JSON.stringify(value)} }).`);
|
|
1420
|
+
}
|
|
1421
|
+
if (typeof value !== 'string') {
|
|
1422
|
+
throw new ValidationError(`[turbine] JSON operator '${op}' on ${column} requires a string, got ${JSON.stringify(value)}.`);
|
|
1423
|
+
}
|
|
1424
|
+
entries.push({ op, pattern, value });
|
|
1425
|
+
}
|
|
1426
|
+
return entries;
|
|
1427
|
+
}
|
|
1340
1428
|
export function jsonRangeEntries(_qi, filter, column) {
|
|
1341
1429
|
const entries = [];
|
|
1342
1430
|
for (const [op, sqlOp] of Object.entries(JSON_RANGE_OPERATORS)) {
|
|
@@ -1367,6 +1455,7 @@ export function jsonRangeEntries(_qi, filter, column) {
|
|
|
1367
1455
|
* stays byte-identical to {@link collectJsonFilterParams}.
|
|
1368
1456
|
*/
|
|
1369
1457
|
export function buildJsonFilterClauses(qi, column, filter, params) {
|
|
1458
|
+
assertJsonFilterKeys(filter, column);
|
|
1370
1459
|
const clauses = [];
|
|
1371
1460
|
// Lazily bind the path once; reuse the same $N in every extraction clause.
|
|
1372
1461
|
let pathParamIdx = null;
|
|
@@ -1407,17 +1496,25 @@ export function buildJsonFilterClauses(qi, column, filter, params) {
|
|
|
1407
1496
|
const lhs = typeof value === 'number' ? castJsonNumeric(qi, extract) : extract;
|
|
1408
1497
|
clauses.push(`${lhs} ${sqlOp} ${qi.p(params.length)}`);
|
|
1409
1498
|
}
|
|
1499
|
+
// Substring comparisons on the extracted path. The operand is LIKE-escaped
|
|
1500
|
+
// exactly like the scalar `contains` family, so a value containing `%` or
|
|
1501
|
+
// `_` matches literally instead of turning into a wildcard.
|
|
1502
|
+
for (const { pattern, value } of jsonStringEntries(filter, column)) {
|
|
1503
|
+
const extract = pathExtract();
|
|
1504
|
+
params.push(pattern(escapeLike(value)));
|
|
1505
|
+
clauses.push(buildLikeClause(qi, extract, qi.p(params.length), filter.mode === 'insensitive'));
|
|
1506
|
+
}
|
|
1410
1507
|
return clauses;
|
|
1411
1508
|
}
|
|
1412
1509
|
/**
|
|
1413
1510
|
* Bind value for a JSON path parameter, encoded per dialect. PostgreSQL's
|
|
1414
|
-
* `#>>` takes a `text[]` (the segments as strings
|
|
1511
|
+
* `#>>` takes a `text[]` (the segments as strings, or `nativeForm` when the
|
|
1415
1512
|
* caller has a specific native binding, e.g. JsonFilter's raw path array).
|
|
1416
1513
|
* Every other engine's JSON function (`json_extract` / `JSON_EXTRACT` /
|
|
1417
1514
|
* `JSON_VALUE`) takes a `'$'`-rooted JSONPath STRING: binding the raw array
|
|
1418
1515
|
* would arrive as `'["a"]'` (the driver shims JSON.stringify non-primitive
|
|
1419
1516
|
* params) and fail at runtime with the engine's bad-JSON-path error. The
|
|
1420
|
-
* encoded path stays a bound parameter
|
|
1517
|
+
* encoded path stays a bound parameter, never spliced into SQL text, so
|
|
1421
1518
|
* the build/collect param mirrors stay in lockstep and injection-safe.
|
|
1422
1519
|
*/
|
|
1423
1520
|
export function jsonPathParam(qi, path, nativeForm) {
|
|
@@ -1429,7 +1526,7 @@ export function jsonPathParam(qi, path, nativeForm) {
|
|
|
1429
1526
|
}
|
|
1430
1527
|
/**
|
|
1431
1528
|
* Cast an extracted JSON path text value to a numeric type for range
|
|
1432
|
-
* comparison. PostgreSQL uses `(expr)::numeric` (exact
|
|
1529
|
+
* comparison. PostgreSQL uses `(expr)::numeric` (exact, the right way to
|
|
1433
1530
|
* compare JSON numbers, and `::float` would lose precision on big ints);
|
|
1434
1531
|
* other dialects route through {@link Dialect.castAggregate} (SQLite/MySQL/
|
|
1435
1532
|
* SQL Server have no `::` operator) as a float cast.
|
package/dist/query/writes.d.ts
CHANGED
|
@@ -20,10 +20,10 @@ import type { BuilderCtx } from './where.js';
|
|
|
20
20
|
*
|
|
21
21
|
* Two value shapes need rewriting, both a JS `Date` on a temporal column:
|
|
22
22
|
*
|
|
23
|
-
* 1. A time-of-day column (`time` / `timetz`)
|
|
23
|
+
* 1. A time-of-day column (`time` / `timetz`), the driver serializes a `Date`
|
|
24
24
|
* as a full ISO timestamp with the process offset and Postgres answers
|
|
25
25
|
* `22007 invalid input syntax for type time`. Rewritten on every engine.
|
|
26
|
-
* 2. A zone-less `date` / `timestamp` column on PostgreSQL
|
|
26
|
+
* 2. A zone-less `date` / `timestamp` column on PostgreSQL, the driver's
|
|
27
27
|
* local-offset serialization stores the PROCESS's calendar fields, so in a
|
|
28
28
|
* non-UTC process the stored value is wrong and (because the read path
|
|
29
29
|
* interprets an offset-less value as UTC) does not round-trip. Rewritten to
|
|
@@ -85,17 +85,36 @@ export declare function piiColumns(_qi: BuilderCtx, meta: TableMetadata): Set<st
|
|
|
85
85
|
export declare function piiFields(_qi: BuilderCtx, meta: TableMetadata): string[];
|
|
86
86
|
/**
|
|
87
87
|
* The `RETURNING` / `OUTPUT` selection for a write on this table. A table with
|
|
88
|
-
* no PII column returns `'*'` (every column
|
|
88
|
+
* no PII column returns `'*'` (every column, byte-identical SQL to before);
|
|
89
89
|
* a table WITH PII columns returns an explicit quoted list of every non-PII
|
|
90
90
|
* column so the PII values never leave the database on a write. A PII-tagged
|
|
91
91
|
* PRIMARY KEY column is kept in the projection regardless (the returned row
|
|
92
|
-
* must stay addressable): tag sensitive data, not keys
|
|
92
|
+
* must stay addressable): tag sensitive data, not keys, a PII PK is
|
|
93
93
|
* documented out of scope for stripping. Writes accept no `select`/`includePii`
|
|
94
94
|
* (unlike reads), so this is the whole write-return policy at the SQL level;
|
|
95
95
|
* {@link parseWriteRow} remains as a defense-in-depth strip (a no-op once the
|
|
96
96
|
* SQL already excludes the columns). Derived purely from static per-table
|
|
97
97
|
* schema metadata, so the write SQL cache needs no extra key segment.
|
|
98
98
|
*/
|
|
99
|
+
/**
|
|
100
|
+
* Return `data` with every `updatedAt`-tagged column the caller did not name
|
|
101
|
+
* set to `now`, or the original object when the table has none.
|
|
102
|
+
*
|
|
103
|
+
* Prisma's `@updatedAt` has no turbine equivalent, so a migrated application
|
|
104
|
+
* had to remember the field on every single update, and the value is usually
|
|
105
|
+
* load-bearing in the response body, so forgetting it is a silent staleness
|
|
106
|
+
* bug rather than a crash. The tag is opt-in per column and never inferred
|
|
107
|
+
* from a column's name, so a schema that does not use it emits byte-identical
|
|
108
|
+
* SQL and an application already managing its own timestamp is untouched.
|
|
109
|
+
*
|
|
110
|
+
* The timestamp is generated CLIENT-side (like Prisma) rather than as a SQL
|
|
111
|
+
* `now()`, so it flows through the same temporal coercion as any other bound
|
|
112
|
+
* `Date` and lands in UTC on every engine.
|
|
113
|
+
*
|
|
114
|
+
* An explicit value always wins, including an explicit `null`: naming the
|
|
115
|
+
* column is a statement of intent.
|
|
116
|
+
*/
|
|
117
|
+
export declare function applyUpdatedAtColumns(qi: BuilderCtx, data: Record<string, unknown>): Record<string, unknown>;
|
|
99
118
|
export declare function writeReturningColumns(qi: BuilderCtx): ReturningSelection;
|
|
100
119
|
/**
|
|
101
120
|
* String form of {@link writeReturningColumns} for a `SELECT` list (the
|
|
@@ -132,7 +151,7 @@ export declare function assertNoGeneratedColumns(qi: BuilderCtx, data: Record<st
|
|
|
132
151
|
*
|
|
133
152
|
* Supports plain values and atomic operator objects ({ set, increment,
|
|
134
153
|
* decrement, multiply, divide }). An operator object is detected ONLY when
|
|
135
|
-
* it has EXACTLY one key that is one of the 5 operator keys
|
|
154
|
+
* it has EXACTLY one key that is one of the 5 operator keys, this avoids
|
|
136
155
|
* misinterpreting JSON column values like `{ set: 'x' }` as operators
|
|
137
156
|
* (real operator objects always have exactly one key, and a plain JSON
|
|
138
157
|
* payload that happens to have a single `set` key is extremely unusual).
|