turbine-orm 0.31.0 → 0.32.1

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/dist/errors.js CHANGED
@@ -144,8 +144,16 @@ export class NotFoundError extends TurbineError {
144
144
  /** Thrown when a query or transaction exceeds the configured timeout */
145
145
  export class TimeoutError extends TurbineError {
146
146
  timeoutMs;
147
- constructor(timeoutMs, context = 'Query') {
148
- super(TurbineErrorCode.TIMEOUT, `[turbine] ${context} timed out after ${timeoutMs}ms`);
147
+ /**
148
+ * @param timeoutMs the client-side timeout budget in ms. Pass `0` when the
149
+ * duration is unknown (e.g. a server-side `statement_timeout` cancellation
150
+ * surfaced via `wrapPgError`, where Turbine did not set the deadline).
151
+ * @param context human label for the operation ("Query", "Transaction").
152
+ * @param options optional `message` override and pg `cause` to preserve, used
153
+ * when wrapping a driver error rather than a client-side timer expiry.
154
+ */
155
+ constructor(timeoutMs, context = 'Query', options) {
156
+ super(TurbineErrorCode.TIMEOUT, options?.message ?? `[turbine] ${context} timed out after ${timeoutMs}ms`, options);
149
157
  this.name = 'TimeoutError';
150
158
  this.timeoutMs = timeoutMs;
151
159
  }
@@ -159,8 +167,13 @@ export class ValidationError extends TurbineError {
159
167
  }
160
168
  /** Thrown when a database connection fails */
161
169
  export class ConnectionError extends TurbineError {
162
- constructor(message) {
163
- super(TurbineErrorCode.CONNECTION, message);
170
+ /**
171
+ * @param message human-readable connection failure description.
172
+ * @param options optional pg/driver `cause` to preserve, used when wrapping a
173
+ * connection-class driver error via `wrapPgError`.
174
+ */
175
+ constructor(message, options) {
176
+ super(TurbineErrorCode.CONNECTION, message, options);
164
177
  this.name = 'ConnectionError';
165
178
  }
166
179
  }
@@ -477,6 +490,35 @@ function parseColumnsFromDetail(detail) {
477
490
  return undefined;
478
491
  return m[1].split(',').map((s) => s.trim());
479
492
  }
493
+ /**
494
+ * Connection-class error codes. Covers both pg SQLSTATEs (class 08
495
+ * connection_exception, plus a few class-53/57 admin/availability codes) and
496
+ * Node driver-level error codes that arrive on the same `.code` field when the
497
+ * socket never reaches Postgres. All map to {@link ConnectionError} (E004).
498
+ *
499
+ * `57014` (query_canceled, a server-side `statement_timeout` cancellation) is
500
+ * intentionally NOT here: it maps to {@link TimeoutError} (E002) instead.
501
+ */
502
+ const CONNECTION_ERROR_CODES = new Set([
503
+ // pg SQLSTATE class 08: connection_exception
504
+ '08000', // connection_exception
505
+ '08001', // sqlclient_unable_to_establish_sqlconnection
506
+ '08003', // connection_does_not_exist
507
+ '08004', // sqlserver_rejected_establishment_of_sqlconnection
508
+ '08006', // connection_failure
509
+ '08P01', // protocol_violation
510
+ // pg SQLSTATE class 53/57 (server unavailable / shutting down)
511
+ '53300', // too_many_connections
512
+ '57P01', // admin_shutdown
513
+ '57P02', // crash_shutdown
514
+ '57P03', // cannot_connect_now
515
+ // Node driver-level socket errors (surface on err.code too)
516
+ 'ECONNREFUSED',
517
+ 'ECONNRESET',
518
+ 'ETIMEDOUT',
519
+ 'ENOTFOUND',
520
+ 'EPIPE',
521
+ ]);
480
522
  /**
481
523
  * Translate a pg driver error into a typed Turbine error.
482
524
  * If the error doesn't match a known constraint code, returns it unchanged.
@@ -489,6 +531,8 @@ function parseColumnsFromDetail(detail) {
489
531
  * 23P01 (exclusion_violation) -> ExclusionConstraintError
490
532
  * 40P01 (deadlock_detected) -> DeadlockError (retryable)
491
533
  * 40001 (serialization_failure) -> SerializationFailureError (retryable)
534
+ * 57014 (query_canceled) -> TimeoutError (server-side statement_timeout)
535
+ * connection-class codes -> ConnectionError (see CONNECTION_ERROR_CODES)
492
536
  *
493
537
  * The original pg error is preserved as `.cause` on the wrapped error.
494
538
  */
@@ -541,7 +585,21 @@ export function wrapPgError(err) {
541
585
  return new SerializationFailureError({
542
586
  cause: err,
543
587
  });
588
+ case '57014':
589
+ // query_canceled: a server-side statement_timeout cancelled the query.
590
+ // Turbine did not set the deadline (that lives in Postgres config), so
591
+ // there is no client-side budget to report → timeoutMs = 0.
592
+ return new TimeoutError(0, 'Query', {
593
+ message: '[turbine] Query canceled by server-side statement_timeout',
594
+ cause: err,
595
+ });
544
596
  default:
597
+ if (CONNECTION_ERROR_CODES.has(e.code)) {
598
+ const pgMessage = typeof e.message === 'string' && e.message.length > 0 ? e.message : undefined;
599
+ return new ConnectionError(pgMessage
600
+ ? `[turbine] Database connection error: ${pgMessage}`
601
+ : `[turbine] Database connection error (${e.code})`, { cause: err });
602
+ }
545
603
  return err;
546
604
  }
547
605
  }
package/dist/generate.js CHANGED
@@ -670,7 +670,8 @@ export function generateIndex(schema, options) {
670
670
  lines.push('/**');
671
671
  lines.push(' * Create a new Turbine client instance.');
672
672
  lines.push(' *');
673
- lines.push(' * @param config - Connection configuration. Falls back to DATABASE_URL env var.');
673
+ lines.push(' * @param config - Connection configuration. Omit it (or pass no connection');
674
+ lines.push(' * fields) to fall back to the `DATABASE_URL` environment variable.');
674
675
  lines.push(' * @returns A fully-typed TurbineClient with table accessors.');
675
676
  lines.push(' */');
676
677
  lines.push('export function turbine(config?: TurbineConfig): TurbineClient {');
package/dist/index.d.ts CHANGED
@@ -43,7 +43,7 @@ export { type IntrospectOptions, introspect } from './introspect.js';
43
43
  export { executeNestedCreate, executeNestedUpdate, hasRelationFields, type NestedWriteContext, } from './nested-write.js';
44
44
  export type { ObserveConfig, ObserveHandle } from './observe.js';
45
45
  export { executePipeline, type PipelineOptions, type PipelineResults, pipelineSupported } from './pipeline.js';
46
- export { type AggregateArgs, type AggregateResult, type ArrayFilter, type ColumnRef, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateDataInput, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GlobalFilters, type GroupByArgs, type HavingClause, type JsonFilter, type JsonPathOrderBy, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderDirection, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type SelectResult, type SkipGlobalFilters, type TextSearchFilter, type TypedWithClause, type UpdateArgs, type UpdateDataInput, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithResult, } from './query/index.js';
46
+ export { type AggregateArgs, type AggregateResult, type ArrayFilter, type ColumnRef, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateDataInput, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GlobalFilters, type GroupByAggregateSpec, type GroupByArgs, type GroupByDistinctOn, type HavingClause, type JsonFilter, type JsonPathAggregateTarget, type JsonPathGroupKey, type JsonPathOrderBy, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderDirection, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type RelationPickBy, type RelationPickOrderBy, type SelectResult, type SkipGlobalFilters, type TextSearchFilter, type TypedWithClause, type UpdateArgs, type UpdateDataInput, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithResult, } from './query/index.js';
47
47
  export { type ActiveSubscription, type NotificationHandler, type Subscription, validateChannel } from './realtime.js';
48
48
  export type { CheckMetadata, ColumnMetadata, IndexMetadata, ReferentialAction, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
49
49
  export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, } from './schema.js';
package/dist/powql.js CHANGED
@@ -38,6 +38,7 @@ import { randomUUID } from 'node:crypto';
38
38
  import { NotFoundError, TimeoutError, UnsupportedFeatureError, ValidationError } from './errors.js';
39
39
  import { executeNestedCreate, executeNestedUpdate, hasRelationFields, } from './nested-write.js';
40
40
  import { PowdbFloatParam, powqlColumnType, quotePowqlIdent, rowToEntity } from './powdb.js';
41
+ import { isRelationPickOrderBy } from './query/filters.js';
41
42
  import { escapeLike } from './query/utils.js';
42
43
  import { normalizeKeyColumns, snakeToCamel, } from './schema.js';
43
44
  /**
@@ -475,7 +476,22 @@ export class PowqlInterface {
475
476
  return '';
476
477
  const parts = keys.map(([field, dir]) => {
477
478
  if (dir && typeof dir === 'object') {
478
- throw new UnsupportedFeatureError('vector / distance ordering', 'PowDB', `field "${field}"`);
479
+ // Name the actual feature in the refusal — a pick-row ordering
480
+ // reported as "vector / distance ordering" sends users hunting for
481
+ // pgvector docs. All object-valued orderings stay E017 on PowDB.
482
+ const o = dir;
483
+ const feature = isRelationPickOrderBy(dir)
484
+ ? 'relation pick-row ordering'
485
+ : 'distance' in o
486
+ ? 'vector / distance ordering'
487
+ : Array.isArray(o.path)
488
+ ? 'JSON-path ordering'
489
+ : '_count' in o
490
+ ? 'relation _count ordering'
491
+ : 'sort' in o || 'nulls' in o
492
+ ? 'NULLS placement / sort-spec ordering'
493
+ : 'object-valued ordering';
494
+ throw new UnsupportedFeatureError(feature, 'PowDB', `field "${field}"`);
479
495
  }
480
496
  return `${this.ref(field)} ${dir === 'desc' ? 'desc' : 'asc'}`;
481
497
  });
@@ -566,7 +582,7 @@ export class PowqlInterface {
566
582
  const limit = args.limit ?? args.take ?? this.defaultLimit;
567
583
  if (limit === undefined && this.warnOnUnlimited && !this.warnedUnlimited) {
568
584
  this.warnedUnlimited = true;
569
- console.warn(`[turbine] findMany on "${this.table}" has no limit this scans the whole table.`);
585
+ console.warn(`[turbine] findMany on "${this.table}" has no limit: this scans the whole table.`);
570
586
  }
571
587
  const limitClause = limit !== undefined ? ` limit ${this.param(limit, params)}` : '';
572
588
  const offsetClause = args.offset ? ` offset ${this.param(args.offset, params)}` : '';
@@ -1119,6 +1135,27 @@ export class PowqlInterface {
1119
1135
  }
1120
1136
  async groupBy(args) {
1121
1137
  return this.withMiddleware('groupBy', args, async () => {
1138
+ // The SQL-only groupBy extensions (DISTINCT ON row source, JSON-path
1139
+ // group keys / aggregate targets) have no PowQL equivalent: refuse
1140
+ // clearly instead of emitting broken PowQL.
1141
+ if (args.distinctOn) {
1142
+ throw new UnsupportedFeatureError('groupBy distinctOn row source', 'PowDB');
1143
+ }
1144
+ for (const entry of args.by) {
1145
+ if (typeof entry !== 'string') {
1146
+ throw new UnsupportedFeatureError('JSON-path groupBy keys', 'PowDB');
1147
+ }
1148
+ }
1149
+ for (const fn of ['_sum', '_avg', '_min', '_max']) {
1150
+ const spec = args[fn];
1151
+ if (!spec)
1152
+ continue;
1153
+ for (const value of Object.values(spec)) {
1154
+ if (value !== undefined && typeof value !== 'boolean') {
1155
+ throw new UnsupportedFeatureError(`JSON-path ${fn} aggregate targets`, 'PowDB');
1156
+ }
1157
+ }
1158
+ }
1122
1159
  const params = [];
1123
1160
  const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
1124
1161
  const where = this.buildWhere(resolvedWhere, params);
@@ -138,6 +138,18 @@ export declare function neededParentKeyFields(parentMeta: TableMetadata, withCla
138
138
  * ({@link ValidationError}) when a named relation is to-one.
139
139
  */
140
140
  export declare function resolveCountRelations(parentMeta: TableMetadata, countSpec: WithCount): RelationDef[];
141
+ /**
142
+ * Reject pick-row relation ordering anywhere inside a `with` tree's orderBy —
143
+ * strategy parity with the join path, which throws this exact E003 at SQL
144
+ * build time (`pickOrderNestedError` in builder.ts). Without this guard the
145
+ * loaders would forward `options.orderBy` as the child reader's TOP-LEVEL
146
+ * findMany orderBy, where the pick shape compiles fine — so the same query
147
+ * would execute on 'batched' but throw on 'join'. Walks the whole tree up
148
+ * front so acceptance never depends on which levels have rows — the batched
149
+ * runners in builder.ts call this BEFORE the base query (a zero-row base
150
+ * result must still reject, exactly like the join strategy's build-time throw).
151
+ */
152
+ export declare function rejectNestedPickOrder(withClause: WithClause): void;
141
153
  /**
142
154
  * Load every relation in `withClause` for `parents` and attach it onto each row
143
155
  * in place. Mirrors the join strategy's output shape exactly. Recurses for nested
@@ -48,6 +48,7 @@
48
48
  */
49
49
  import { CircularRelationError, RelationError, UnsupportedFeatureError, ValidationError } from '../errors.js';
50
50
  import { normalizeKeyColumns } from '../schema.js';
51
+ import { isRelationPickOrderBy } from './filters.js';
51
52
  /**
52
53
  * Max parent keys per follow-up query. On Postgres the whole key set travels as
53
54
  * ONE array parameter (`= ANY($1)`), so this is not a bind-parameter limit — it
@@ -177,6 +178,34 @@ export function resolveCountRelations(parentMeta, countSpec) {
177
178
  function keyOf(value) {
178
179
  return String(value);
179
180
  }
181
+ /**
182
+ * Reject pick-row relation ordering anywhere inside a `with` tree's orderBy —
183
+ * strategy parity with the join path, which throws this exact E003 at SQL
184
+ * build time (`pickOrderNestedError` in builder.ts). Without this guard the
185
+ * loaders would forward `options.orderBy` as the child reader's TOP-LEVEL
186
+ * findMany orderBy, where the pick shape compiles fine — so the same query
187
+ * would execute on 'batched' but throw on 'join'. Walks the whole tree up
188
+ * front so acceptance never depends on which levels have rows — the batched
189
+ * runners in builder.ts call this BEFORE the base query (a zero-row base
190
+ * result must still reject, exactly like the join strategy's build-time throw).
191
+ */
192
+ export function rejectNestedPickOrder(withClause) {
193
+ for (const spec of Object.values(withClause)) {
194
+ if (!spec || spec === true)
195
+ continue;
196
+ const options = spec;
197
+ if (options.orderBy) {
198
+ for (const [key, value] of Object.entries(options.orderBy)) {
199
+ if (isRelationPickOrderBy(value)) {
200
+ throw new ValidationError(`[turbine] Pick-row ordering on relation "${key}" is only supported in a top-level ` +
201
+ 'findMany orderBy: nested `with` orderBy does not support it.');
202
+ }
203
+ }
204
+ }
205
+ if (options.with)
206
+ rejectNestedPickOrder(options.with);
207
+ }
208
+ }
180
209
  /**
181
210
  * Load every relation in `withClause` for `parents` and attach it onto each row
182
211
  * in place. Mirrors the join strategy's output shape exactly. Recurses for nested
@@ -185,6 +214,10 @@ function keyOf(value) {
185
214
  export async function loadRelationsBatched(ctx, parents, withClause, timeout, depth = 0, path = [ctx.parentMeta.name]) {
186
215
  if (depth >= MAX_DEPTH)
187
216
  throw new CircularRelationError([...path, '…']);
217
+ // Scope-rule parity with the join strategy: validate the whole tree BEFORE
218
+ // the empty-parents early return, so accept/reject never depends on data.
219
+ if (depth === 0)
220
+ rejectNestedPickOrder(withClause);
188
221
  if (parents.length === 0)
189
222
  return;
190
223
  // Sibling relations are independent (each writes only its own parent[relName]
@@ -22,6 +22,15 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
22
22
  private readonly tableMeta;
23
23
  /** SQL template cache: cacheKey → SqlCacheEntry (sql + prepared statement name) */
24
24
  private readonly sqlTemplateCache;
25
+ /**
26
+ * Whether the most recent {@link acquireSql} call was a cache HIT. Read by
27
+ * {@link crossCheckCache} to decide whether to run the dev-mode lockstep
28
+ * cross-check. Safe as a single mutable flag: each `build*()` method calls
29
+ * `acquireSql` then `crossCheckCache` synchronously with no intervening
30
+ * `await` and no re-entrant `acquireSql` (relation subqueries are built
31
+ * inline, not through the top-level cache).
32
+ */
33
+ private lastCacheHit;
25
34
  private readonly middlewares;
26
35
  private readonly defaultLimit?;
27
36
  private readonly warnOnUnlimited;
@@ -183,8 +192,42 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
183
192
  * On hit, increments counters and returns the cached entry.
184
193
  *
185
194
  * When `sqlCache` is disabled, always calls `build()` without caching.
195
+ *
196
+ * `build` receives a fresh `$N` param scratch array. On a miss those params
197
+ * are discarded (the returned params come from each call site's dedicated
198
+ * collect path); the array exists so the build path can number placeholders
199
+ * via `params.length` exactly as it does today. On a HIT, `build` is skipped
200
+ * here but re-run by {@link crossCheckCache} (dev only) with a fresh array to
201
+ * verify the collect path stayed in lockstep with the build path.
202
+ *
203
+ * Sets {@link lastCacheHit} so the caller's `crossCheckCache` knows whether a
204
+ * cross-check is warranted.
186
205
  */
187
206
  private acquireSql;
207
+ /**
208
+ * Dev-mode SQL-cache lockstep cross-check (see {@link cacheCrossCheckEnabled}).
209
+ *
210
+ * Runs only when the most recent {@link acquireSql} was a cache HIT and the
211
+ * check is enabled. Rebuilds the SQL + `$N` params fresh via the same `build`
212
+ * closure the caller passed to `acquireSql`, then compares:
213
+ * (a) the cached SQL string byte-for-byte against the fresh SQL, and
214
+ * (b) the params the cache-hit collect path produced against the fresh
215
+ * build-path params (length and element-wise strict deep-equal).
216
+ *
217
+ * A mismatch means the fingerprint / build / collect paths have drifted out
218
+ * of lockstep (the exact class of bug that has silently corrupted results
219
+ * before), so it throws a {@link ValidationError} (E003) naming the
220
+ * fingerprint, the operation, and both SQL strings (truncated). Failing loud
221
+ * in dev/test is the point. Production never reaches the comparison.
222
+ *
223
+ * @param op human label of the calling build method (for the error message).
224
+ * @param cacheKey the cache fingerprint that HIT.
225
+ * @param entry the cached SQL entry that will be executed.
226
+ * @param build the same closure passed to `acquireSql`; re-run here to
227
+ * capture the fresh build-path SQL + params.
228
+ * @param collectedParams the params the caller's collect path produced.
229
+ */
230
+ private crossCheckCache;
188
231
  /**
189
232
  * Reset the per-instance unlimited-query warning dedupe set.
190
233
  * Exposed for tests so a single test process can verify the warning fires
@@ -339,6 +382,27 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
339
382
  buildCount(args?: CountArgs<T>): DeferredQuery<number>;
340
383
  groupBy(args: GroupByArgs<T>): Promise<Record<string, unknown>[]>;
341
384
  buildGroupBy(args: GroupByArgs<T>): DeferredQuery<Record<string, unknown>[]>;
385
+ /**
386
+ * Validate a JSON-path target (group key or aggregate target) in groupBy:
387
+ * the field must resolve to a real json/jsonb column and the path must be a
388
+ * non-empty array of keys/indexes. Returns the resolved snake_case column.
389
+ */
390
+ private resolveJsonPathTarget;
391
+ /**
392
+ * Build the `distinctOn` row source for groupBy (PostgreSQL only: other
393
+ * engines throw {@link UnsupportedFeatureError} E017):
394
+ *
395
+ * ```sql
396
+ * (SELECT DISTINCT ON ("c1") * FROM "table"<WHERE> ORDER BY "c1", <orderBy>) AS "table"
397
+ * ```
398
+ *
399
+ * The wrapper is aliased as the table name so every outer expression (group
400
+ * keys, aggregates, HAVING, ORDER BY) is byte-identical to the plain path.
401
+ * `distinctOn.orderBy` is required (it decides which row survives) and
402
+ * supports plain columns, {@link OrderBySpec} nulls, and JSON-path specs;
403
+ * JSON paths push their text[] param here, after the WHERE params.
404
+ */
405
+ private buildDistinctOnSource;
342
406
  /**
343
407
  * Build the SQL fragments for a {@link HavingClause}.
344
408
  *
@@ -349,6 +413,12 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
349
413
  * comparison value is pushed onto the shared `params` array and referenced by
350
414
  * a `$N` placeholder via {@link buildHavingNumericClauses} — there is no string
351
415
  * interpolation of user values.
416
+ *
417
+ * `jsonAggExprs` (from {@link buildGroupBy}) maps `alias:aggKey` to the
418
+ * exact aggregate expression a JSON-path aggregate emitted in SELECT
419
+ * (including its already-bound path placeholder), so HAVING on a JSON-path
420
+ * aggregate alias reuses the same expression instead of resolving the alias
421
+ * as a column.
352
422
  */
353
423
  private buildHavingClauses;
354
424
  /**
@@ -712,6 +782,46 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
712
782
  * the correlation parent is the relation's alias, not `this.table`.
713
783
  */
714
784
  private buildRelationOrderBy;
785
+ /**
786
+ * Validate a {@link RelationPickOrderBy} entry's scope and shape. Shared by
787
+ * the SQL-build path ({@link buildRelationPickOrderBy}) and the cache-hit
788
+ * param-collect mirror ({@link collectRelationPickOrderParams}) so both
789
+ * always throw identically:
790
+ *
791
+ * - `nested` (inside a relation subquery's orderBy or a pick.orderBy):
792
+ * top-level findMany only in this release (E003),
793
+ * - manyToMany: not supported (E003 naming the limitation),
794
+ * - to-one: order by the target column directly instead (E003),
795
+ * - `pick.orderBy` is REQUIRED (deterministic row choice),
796
+ * - `by` must be a target column name or a `{ field, path }` JSON-path spec.
797
+ */
798
+ private pickOrderNestedError;
799
+ private validatePickOrderBy;
800
+ /**
801
+ * Compile a {@link RelationPickOrderBy} term: a correlated scalar subquery
802
+ * that picks ONE related row (`ORDER BY <pick.orderBy> LIMIT 1`, optionally
803
+ * filtered by `pick.where` and the target's global filter) and surfaces one
804
+ * value from it (a plain target column or a JSON-path extraction) as the
805
+ * parent ORDER BY key:
806
+ *
807
+ * ```sql
808
+ * (SELECT ord0."data" #>> $1::text[] FROM "versions" ord0
809
+ * WHERE ord0."instance_id" = "instances"."id" AND ord0."is_current" = $2
810
+ * ORDER BY ord0."created_at" DESC LIMIT 1) ASC NULLS LAST
811
+ * ```
812
+ *
813
+ * Param-push order (mirrored EXACTLY by
814
+ * {@link collectRelationPickOrderParams}): `by` JSON path (if any) →
815
+ * target global filter → `pick.where` → `pick.orderBy` JSON paths.
816
+ */
817
+ private buildRelationPickOrderBy;
818
+ /**
819
+ * Param-collect mirror of {@link buildRelationPickOrderBy}: re-runs the same
820
+ * validation (a warmed cache can never skip it), then pushes in the same
821
+ * order: `by` JSON path → target global filter → `pick.where` →
822
+ * `pick.orderBy` JSON paths.
823
+ */
824
+ private collectRelationPickOrderParams;
715
825
  /**
716
826
  * Compile the ORDER BY terms of a relation `with` clause against the
717
827
  * relation's table alias. One unified path for every relation shape
@@ -1009,6 +1119,13 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
1009
1119
  * Used to detect JSONB/array columns for specialized operators.
1010
1120
  * Uses pre-computed Map for O(1) lookup instead of linear scan.
1011
1121
  */
1122
+ /**
1123
+ * Case-insensitive json/jsonb column-type check. Postgres reports lowercase
1124
+ * udt_names, but SQLite/MySQL introspection surfaces the DECLARED type
1125
+ * (e.g. `JSON`), so every JSON-feature gate compares through this predicate
1126
+ * — build and collect sides alike, keeping the SQL-cache lockstep.
1127
+ */
1128
+ private isJsonColumnType;
1012
1129
  private getColumnPgType;
1013
1130
  /**
1014
1131
  * Get the Postgres base element type for an array column.
@@ -1033,6 +1150,18 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
1033
1150
  * stays byte-identical to {@link collectJsonFilterParams}.
1034
1151
  */
1035
1152
  private buildJsonFilterClauses;
1153
+ /**
1154
+ * Bind value for a JSON path parameter, encoded per dialect. PostgreSQL's
1155
+ * `#>>` takes a `text[]` (the segments as strings — or `nativeForm` when the
1156
+ * caller has a specific native binding, e.g. JsonFilter's raw path array).
1157
+ * Every other engine's JSON function (`json_extract` / `JSON_EXTRACT` /
1158
+ * `JSON_VALUE`) takes a `'$'`-rooted JSONPath STRING: binding the raw array
1159
+ * would arrive as `'["a"]'` (the driver shims JSON.stringify non-primitive
1160
+ * params) and fail at runtime with the engine's bad-JSON-path error. The
1161
+ * encoded path stays a bound parameter — never spliced into SQL text — so
1162
+ * the build/collect param mirrors stay in lockstep and injection-safe.
1163
+ */
1164
+ private jsonPathParam;
1036
1165
  /**
1037
1166
  * Cast an extracted JSON path text value to a numeric type for range
1038
1167
  * comparison. PostgreSQL uses `(expr)::numeric` (exact — the right way to