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.
Files changed (186) hide show
  1. package/README.md +66 -66
  2. package/dist/adapters/cockroachdb.d.ts +5 -5
  3. package/dist/adapters/cockroachdb.js +10 -10
  4. package/dist/adapters/index.d.ts +5 -5
  5. package/dist/adapters/index.js +7 -7
  6. package/dist/adapters/yugabytedb.d.ts +7 -7
  7. package/dist/adapters/yugabytedb.js +10 -10
  8. package/dist/cjs/adapters/cockroachdb.d.ts +5 -5
  9. package/dist/cjs/adapters/cockroachdb.js +10 -10
  10. package/dist/cjs/adapters/index.d.ts +5 -5
  11. package/dist/cjs/adapters/index.js +7 -7
  12. package/dist/cjs/adapters/yugabytedb.d.ts +7 -7
  13. package/dist/cjs/adapters/yugabytedb.js +10 -10
  14. package/dist/cjs/cli/config.d.ts +13 -2
  15. package/dist/cjs/cli/config.js +3 -2
  16. package/dist/cjs/cli/destructive.d.ts +1 -1
  17. package/dist/cjs/cli/destructive.js +1 -1
  18. package/dist/cjs/cli/index.d.ts +10 -10
  19. package/dist/cjs/cli/index.js +49 -45
  20. package/dist/cjs/cli/loader.d.ts +7 -7
  21. package/dist/cjs/cli/loader.js +9 -9
  22. package/dist/cjs/cli/mcp.js +4 -4
  23. package/dist/cjs/cli/migrate.d.ts +5 -5
  24. package/dist/cjs/cli/migrate.js +11 -11
  25. package/dist/cjs/cli/studio-ui.generated.js +1 -1
  26. package/dist/cjs/cli/ui.d.ts +2 -2
  27. package/dist/cjs/cli/ui.js +2 -2
  28. package/dist/cjs/client.d.ts +49 -38
  29. package/dist/cjs/client.js +57 -56
  30. package/dist/cjs/dialect.d.ts +62 -18
  31. package/dist/cjs/dialect.js +40 -2
  32. package/dist/cjs/errors.d.ts +5 -5
  33. package/dist/cjs/errors.js +11 -11
  34. package/dist/cjs/generate.d.ts +6 -6
  35. package/dist/cjs/generate.js +31 -29
  36. package/dist/cjs/index-advisor.d.ts +5 -5
  37. package/dist/cjs/index-advisor.js +0 -0
  38. package/dist/cjs/index.d.ts +1 -1
  39. package/dist/cjs/index.js +7 -7
  40. package/dist/cjs/introspect.d.ts +35 -9
  41. package/dist/cjs/introspect.js +83 -32
  42. package/dist/cjs/mssql.d.ts +11 -11
  43. package/dist/cjs/mssql.js +64 -29
  44. package/dist/cjs/mysql.d.ts +8 -8
  45. package/dist/cjs/mysql.js +61 -23
  46. package/dist/cjs/nested-write.d.ts +21 -2
  47. package/dist/cjs/nested-write.js +51 -14
  48. package/dist/cjs/optional-peer-import.cjs +7 -7
  49. package/dist/cjs/optional-peer-import.d.cts +7 -7
  50. package/dist/cjs/pipeline-submittable.d.ts +2 -2
  51. package/dist/cjs/pipeline-submittable.js +6 -6
  52. package/dist/cjs/pipeline.d.ts +1 -1
  53. package/dist/cjs/pipeline.js +4 -4
  54. package/dist/cjs/powdb-introspect.d.ts +1 -1
  55. package/dist/cjs/powdb-introspect.js +1 -1
  56. package/dist/cjs/powdb.d.ts +28 -28
  57. package/dist/cjs/powdb.js +66 -66
  58. package/dist/cjs/powql.d.ts +27 -27
  59. package/dist/cjs/powql.js +73 -52
  60. package/dist/cjs/query/aggregates.d.ts +1 -1
  61. package/dist/cjs/query/aggregates.js +5 -5
  62. package/dist/cjs/query/batched-loader.d.ts +11 -11
  63. package/dist/cjs/query/batched-loader.js +24 -24
  64. package/dist/cjs/query/builder.d.ts +39 -21
  65. package/dist/cjs/query/builder.js +99 -57
  66. package/dist/cjs/query/compound-unique.d.ts +1 -1
  67. package/dist/cjs/query/compound-unique.js +0 -0
  68. package/dist/cjs/query/deferred.d.ts +12 -6
  69. package/dist/cjs/query/deferred.js +1 -1
  70. package/dist/cjs/query/filters.d.ts +31 -11
  71. package/dist/cjs/query/filters.js +67 -14
  72. package/dist/cjs/query/index.d.ts +1 -1
  73. package/dist/cjs/query/index.js +1 -1
  74. package/dist/cjs/query/relations.d.ts +9 -9
  75. package/dist/cjs/query/relations.js +164 -57
  76. package/dist/cjs/query/types.d.ts +86 -35
  77. package/dist/cjs/query/types.js +1 -1
  78. package/dist/cjs/query/utils.d.ts +27 -10
  79. package/dist/cjs/query/utils.js +86 -14
  80. package/dist/cjs/query/where.d.ts +47 -28
  81. package/dist/cjs/query/where.js +130 -31
  82. package/dist/cjs/query/writes.d.ts +24 -5
  83. package/dist/cjs/query/writes.js +102 -13
  84. package/dist/cjs/realtime.d.ts +7 -7
  85. package/dist/cjs/realtime.js +9 -9
  86. package/dist/cjs/schema-builder.d.ts +18 -7
  87. package/dist/cjs/schema-builder.js +17 -10
  88. package/dist/cjs/schema-metadata.d.ts +3 -3
  89. package/dist/cjs/schema-metadata.js +9 -9
  90. package/dist/cjs/schema-sql.d.ts +9 -9
  91. package/dist/cjs/schema-sql.js +20 -20
  92. package/dist/cjs/schema.d.ts +19 -9
  93. package/dist/cjs/schema.js +6 -6
  94. package/dist/cjs/serverless.d.ts +15 -15
  95. package/dist/cjs/serverless.js +16 -16
  96. package/dist/cjs/sqlite.d.ts +8 -8
  97. package/dist/cjs/sqlite.js +53 -22
  98. package/dist/cjs/typed-sql.d.ts +4 -4
  99. package/dist/cjs/typed-sql.js +5 -5
  100. package/dist/cli/config.d.ts +13 -2
  101. package/dist/cli/config.js +3 -2
  102. package/dist/cli/destructive.d.ts +1 -1
  103. package/dist/cli/destructive.js +1 -1
  104. package/dist/cli/index.d.ts +10 -10
  105. package/dist/cli/index.js +49 -45
  106. package/dist/cli/loader.d.ts +7 -7
  107. package/dist/cli/loader.js +9 -9
  108. package/dist/cli/mcp.js +4 -4
  109. package/dist/cli/migrate.d.ts +5 -5
  110. package/dist/cli/migrate.js +11 -11
  111. package/dist/cli/studio-ui.generated.js +1 -1
  112. package/dist/cli/ui.d.ts +2 -2
  113. package/dist/cli/ui.js +2 -2
  114. package/dist/client.d.ts +49 -38
  115. package/dist/client.js +57 -56
  116. package/dist/dialect.d.ts +62 -18
  117. package/dist/dialect.js +40 -2
  118. package/dist/errors.d.ts +5 -5
  119. package/dist/errors.js +11 -11
  120. package/dist/generate.d.ts +6 -6
  121. package/dist/generate.js +31 -29
  122. package/dist/index-advisor.d.ts +5 -5
  123. package/dist/index-advisor.js +0 -0
  124. package/dist/index.d.ts +1 -1
  125. package/dist/index.js +7 -7
  126. package/dist/introspect.d.ts +35 -9
  127. package/dist/introspect.js +82 -32
  128. package/dist/mssql.d.ts +11 -11
  129. package/dist/mssql.js +64 -29
  130. package/dist/mysql.d.ts +8 -8
  131. package/dist/mysql.js +61 -23
  132. package/dist/nested-write.d.ts +21 -2
  133. package/dist/nested-write.js +51 -14
  134. package/dist/optional-peer-import.cjs +7 -7
  135. package/dist/optional-peer-import.d.cts +7 -7
  136. package/dist/pipeline-submittable.d.ts +2 -2
  137. package/dist/pipeline-submittable.js +6 -6
  138. package/dist/pipeline.d.ts +1 -1
  139. package/dist/pipeline.js +4 -4
  140. package/dist/powdb-introspect.d.ts +1 -1
  141. package/dist/powdb-introspect.js +1 -1
  142. package/dist/powdb.d.ts +28 -28
  143. package/dist/powdb.js +66 -66
  144. package/dist/powql.d.ts +27 -27
  145. package/dist/powql.js +73 -52
  146. package/dist/query/aggregates.d.ts +1 -1
  147. package/dist/query/aggregates.js +5 -5
  148. package/dist/query/batched-loader.d.ts +11 -11
  149. package/dist/query/batched-loader.js +24 -24
  150. package/dist/query/builder.d.ts +39 -21
  151. package/dist/query/builder.js +100 -58
  152. package/dist/query/compound-unique.d.ts +1 -1
  153. package/dist/query/compound-unique.js +0 -0
  154. package/dist/query/deferred.d.ts +12 -6
  155. package/dist/query/deferred.js +1 -1
  156. package/dist/query/filters.d.ts +31 -11
  157. package/dist/query/filters.js +66 -13
  158. package/dist/query/index.d.ts +1 -1
  159. package/dist/query/index.js +1 -1
  160. package/dist/query/relations.d.ts +9 -9
  161. package/dist/query/relations.js +165 -58
  162. package/dist/query/types.d.ts +86 -35
  163. package/dist/query/types.js +1 -1
  164. package/dist/query/utils.d.ts +27 -10
  165. package/dist/query/utils.js +84 -14
  166. package/dist/query/where.d.ts +47 -28
  167. package/dist/query/where.js +129 -32
  168. package/dist/query/writes.d.ts +24 -5
  169. package/dist/query/writes.js +101 -13
  170. package/dist/realtime.d.ts +7 -7
  171. package/dist/realtime.js +9 -9
  172. package/dist/schema-builder.d.ts +18 -7
  173. package/dist/schema-builder.js +17 -10
  174. package/dist/schema-metadata.d.ts +3 -3
  175. package/dist/schema-metadata.js +9 -9
  176. package/dist/schema-sql.d.ts +9 -9
  177. package/dist/schema-sql.js +20 -20
  178. package/dist/schema.d.ts +19 -9
  179. package/dist/schema.js +6 -6
  180. package/dist/serverless.d.ts +15 -15
  181. package/dist/serverless.js +16 -16
  182. package/dist/sqlite.d.ts +8 -8
  183. package/dist/sqlite.js +53 -22
  184. package/dist/typed-sql.d.ts +4 -4
  185. package/dist/typed-sql.js +5 -5
  186. package/package.json +2 -2
@@ -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 — the one value both sides pass identically.
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 — so a single scoped build/collect/fingerprint
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)` — `"target".` for EXISTS sub-wheres, `t0.` for aliases. */
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 — the form used inside relation-filter `EXISTS`
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 — the compiled emptiness, NOT the
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 — used to fingerprint a sub-where
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}) — the emitted SQL is byte-identical to the
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 — unlike the top-level {@link buildScalarClause}
296
- * — those shapes are not special-cased here and keep their historical
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 — it never changes the emitted SQL — so the
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 — bulk-insert forms like `UNNEST($1::text[])` otherwise type the
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 — `defineSchema` and the other engines leave it empty), so
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 — equality, IS NULL, operator objects (incl.
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 — equality vs null vs operator sets vs combinators — or two
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 — a user-supplied string can never become a SQL operator.
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' } } }` — is an implicit `is`
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
- * — build and collect sides alike, keeping the SQL-cache lockstep.
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
- * Validate and enumerate the range comparisons (`gt`/`gte`/`lt`/`lte`) on a
482
- * JSON filter, in the fixed {@link JSON_RANGE_OPERATORS} order. Shared by
483
- * the SQL-build path ({@link buildJsonFilterClauses}) and the cache-hit
484
- * param-collect path ({@link collectJsonFilterParams}) so both always agree
485
- * on which params are pushed — and both throw identically for invalid
486
- * shapes, so a warmed cache can never skip validation.
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 — or `nativeForm` when the
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 — never spliced into SQL text — so
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 — the right way to
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.
@@ -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 — otherwise `every` is trivially true and no subquery is built.
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 — treat it as absent so it never emits a dangling clause.
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 — the form used inside relation-filter `EXISTS`
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 — the compiled emptiness, NOT the
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 — used to fingerprint a sub-where
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}) — the emitted SQL is byte-identical to the
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 — unlike the top-level {@link buildScalarClause}
669
- * — those shapes are not special-cased here and keep their historical
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 — mirrored
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) — gf irrelevant.
909
+ // "every" with empty filter = true (all match trivially), gf irrelevant.
905
910
  }
906
911
  }
907
- // "is": EXISTS — for to-one relations (same SQL as "some").
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 — for to-one relations (same SQL as "none").
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 — it never changes the emitted SQL — so the
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 — bulk-insert forms like `UNNEST($1::text[])` otherwise type the
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 — `defineSchema` and the other engines leave it empty), so
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 — search_path
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 — equality, IS NULL, operator objects (incl.
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 — equality vs null vs operator sets vs combinators — or two
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 buildLikeClause = (paramRef) => op.mode === 'insensitive' ? qi.dialect.buildInsensitiveLike(column, paramRef) : `${column} LIKE ${paramRef}`;
1190
+ const insensitive = op.mode === 'insensitive';
1186
1191
  if (op.contains !== undefined) {
1187
1192
  params.push(`%${escapeLike(op.contains)}%`);
1188
- clauses.push(`${buildLikeClause(qi.p(params.length))} ESCAPE '\\'`);
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(`${buildLikeClause(qi.p(params.length))} ESCAPE '\\'`);
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(`${buildLikeClause(qi.p(params.length))} ESCAPE '\\'`);
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 — a user-supplied string can never become a SQL operator.
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' } } }` — is an implicit `is`
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
- * — build and collect sides alike, keeping the SQL-cache lockstep.
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 — and both throw identically for invalid
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 — or `nativeForm` when the
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 — never spliced into SQL text — so
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 — the right way to
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.
@@ -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`) — the driver serializes a `Date`
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 — the driver's
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 — byte-identical SQL to before);
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 — a PII PK is
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 — this avoids
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).