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/README.md +28 -19
- package/dist/cjs/cli/config.js +76 -16
- package/dist/cjs/cli/index.js +174 -19
- package/dist/cjs/client.js +16 -0
- package/dist/cjs/errors.js +62 -4
- package/dist/cjs/generate.js +2 -1
- package/dist/cjs/powql.js +39 -2
- package/dist/cjs/query/batched-loader.js +34 -0
- package/dist/cjs/query/builder.js +686 -121
- package/dist/cjs/query/filters.js +28 -0
- package/dist/cli/config.d.ts +53 -1
- package/dist/cli/config.js +73 -16
- package/dist/cli/index.d.ts +68 -0
- package/dist/cli/index.js +173 -21
- package/dist/client.js +16 -0
- package/dist/errors.d.ts +22 -2
- package/dist/errors.js +62 -4
- package/dist/generate.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/powql.js +39 -2
- package/dist/query/batched-loader.d.ts +12 -0
- package/dist/query/batched-loader.js +33 -0
- package/dist/query/builder.d.ts +129 -0
- package/dist/query/builder.js +688 -123
- package/dist/query/deferred.d.ts +10 -1
- package/dist/query/filters.d.ts +13 -1
- package/dist/query/filters.js +27 -0
- package/dist/query/index.d.ts +1 -1
- package/dist/query/types.d.ts +148 -12
- package/package.json +3 -2
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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]
|
package/dist/query/builder.d.ts
CHANGED
|
@@ -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
|