turbine-orm 0.39.0 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -3
- package/dist/cjs/cli/index.js +500 -130
- package/dist/cjs/cli/migrate.js +79 -0
- package/dist/cjs/client.js +1 -0
- package/dist/cjs/powql.js +7 -3
- package/dist/cjs/query/aggregates.js +11 -1
- package/dist/cjs/query/builder.js +35 -8
- package/dist/cjs/query/filters.js +33 -0
- package/dist/cjs/query/relations.js +7 -7
- package/dist/cli/index.d.ts +61 -1
- package/dist/cli/index.js +500 -131
- package/dist/cli/migrate.d.ts +33 -0
- package/dist/cli/migrate.js +78 -0
- package/dist/client.d.ts +10 -0
- package/dist/client.js +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/powql.js +8 -4
- package/dist/query/aggregates.d.ts +1 -1
- package/dist/query/aggregates.js +12 -2
- package/dist/query/builder.d.ts +15 -3
- package/dist/query/builder.js +36 -9
- package/dist/query/deferred.d.ts +11 -0
- package/dist/query/filters.d.ts +17 -0
- package/dist/query/filters.js +32 -0
- package/dist/query/index.d.ts +1 -1
- package/dist/query/relations.js +8 -8
- package/dist/query/types.d.ts +86 -4
- package/package.json +3 -3
package/dist/cli/migrate.d.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { DatabaseAdapter } from '../adapters/index.js';
|
|
15
15
|
import { type Dialect } from '../dialect.js';
|
|
16
|
+
import { type DestructiveStatement } from './destructive.js';
|
|
16
17
|
export interface MigrationFile {
|
|
17
18
|
/** Full filename (e.g. "20260325120000_create_users.sql") */
|
|
18
19
|
filename: string;
|
|
@@ -91,6 +92,38 @@ export interface MigrationRecipe {
|
|
|
91
92
|
* in here without touching {@link createMigration} or the CLI handler.
|
|
92
93
|
*/
|
|
93
94
|
export declare const MIGRATION_RECIPES: Record<string, MigrationRecipe>;
|
|
95
|
+
/**
|
|
96
|
+
* The UP/DOWN body produced by {@link buildDiffMigrationBody}, plus the
|
|
97
|
+
* destructive statements found in each direction so the CLI can warn loudly.
|
|
98
|
+
*/
|
|
99
|
+
export interface DiffMigrationBody {
|
|
100
|
+
/** Annotated UP SQL (destructive statements flagged with loud comments). */
|
|
101
|
+
up: string;
|
|
102
|
+
/** Annotated DOWN SQL, or an irreversible placeholder when none is derivable. */
|
|
103
|
+
down: string;
|
|
104
|
+
/** Destructive statements detected in the UP direction. */
|
|
105
|
+
destructiveUp: DestructiveStatement[];
|
|
106
|
+
/** Destructive statements detected in the DOWN direction. */
|
|
107
|
+
destructiveDown: DestructiveStatement[];
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Build a migration UP/DOWN body from a `schemaDiff()` result.
|
|
111
|
+
*
|
|
112
|
+
* - UP is the diff's forward statements. DOWN is the diff's reverse statements
|
|
113
|
+
* when derivable, otherwise a clearly-commented "irreversible" placeholder.
|
|
114
|
+
* - Destructive statements in EITHER direction (a lossy `ALTER COLUMN ... TYPE`
|
|
115
|
+
* in UP, a `DROP TABLE`/`DROP COLUMN` reverse in DOWN) are flagged inline and,
|
|
116
|
+
* when any exist, a loud file-level banner is prepended to UP.
|
|
117
|
+
* - Any diff `warnings` (changes the diff refuses to apply automatically, e.g.
|
|
118
|
+
* enum value removals) are surfaced as `-- NOTE:` comments in UP.
|
|
119
|
+
*
|
|
120
|
+
* Pure and DB-free, so it is unit-testable from a synthesized diff.
|
|
121
|
+
*/
|
|
122
|
+
export declare function buildDiffMigrationBody(diff: {
|
|
123
|
+
statements: string[];
|
|
124
|
+
reverseStatements: string[];
|
|
125
|
+
warnings?: string[];
|
|
126
|
+
}): DiffMigrationBody;
|
|
94
127
|
/**
|
|
95
128
|
* Create a new migration file.
|
|
96
129
|
*
|
package/dist/cli/migrate.js
CHANGED
|
@@ -202,6 +202,84 @@ export const MIGRATION_RECIPES = {
|
|
|
202
202
|
build: buildBackfillRecipe,
|
|
203
203
|
},
|
|
204
204
|
};
|
|
205
|
+
/** Loud file-level banner prepended when a diff migration contains destructive statements. */
|
|
206
|
+
const DESTRUCTIVE_MIGRATION_BANNER = [
|
|
207
|
+
'-- ============================================================',
|
|
208
|
+
'-- WARNING: this migration contains DESTRUCTIVE statement(s).',
|
|
209
|
+
'-- Each one is flagged inline below. `turbine migrate up` refuses',
|
|
210
|
+
'-- destructive statements by default: it asks you to confirm',
|
|
211
|
+
'-- interactively, or you must pass --allow-destructive. Review',
|
|
212
|
+
'-- every flagged statement carefully before running.',
|
|
213
|
+
'-- ============================================================',
|
|
214
|
+
];
|
|
215
|
+
/**
|
|
216
|
+
* Annotate a list of SQL statements: scan each for data-destroying operations
|
|
217
|
+
* (via {@link scanDestructiveSql}) and prefix any offender with loud, commented
|
|
218
|
+
* warnings. Statements are left intact so the existing `migrate up` gate still
|
|
219
|
+
* refuses them by default; the comments just make the danger visible on review.
|
|
220
|
+
*/
|
|
221
|
+
function annotateDiffStatements(statements) {
|
|
222
|
+
const destructive = [];
|
|
223
|
+
const out = [];
|
|
224
|
+
for (const raw of statements) {
|
|
225
|
+
const stmt = raw.trim();
|
|
226
|
+
if (!stmt)
|
|
227
|
+
continue;
|
|
228
|
+
const hits = scanDestructiveSql(stmt);
|
|
229
|
+
if (hits.length > 0) {
|
|
230
|
+
destructive.push(...hits);
|
|
231
|
+
for (const h of hits) {
|
|
232
|
+
out.push(`-- !! DESTRUCTIVE [${h.kind}] ${h.target}: ${DESTRUCTIVE_KIND_LABEL[h.kind]}`);
|
|
233
|
+
}
|
|
234
|
+
out.push('-- !! Refused by default. Confirm interactively or pass --allow-destructive to run it.');
|
|
235
|
+
}
|
|
236
|
+
out.push(stmt.endsWith(';') ? stmt : `${stmt};`);
|
|
237
|
+
}
|
|
238
|
+
return { text: out.join('\n'), destructive };
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Build a migration UP/DOWN body from a `schemaDiff()` result.
|
|
242
|
+
*
|
|
243
|
+
* - UP is the diff's forward statements. DOWN is the diff's reverse statements
|
|
244
|
+
* when derivable, otherwise a clearly-commented "irreversible" placeholder.
|
|
245
|
+
* - Destructive statements in EITHER direction (a lossy `ALTER COLUMN ... TYPE`
|
|
246
|
+
* in UP, a `DROP TABLE`/`DROP COLUMN` reverse in DOWN) are flagged inline and,
|
|
247
|
+
* when any exist, a loud file-level banner is prepended to UP.
|
|
248
|
+
* - Any diff `warnings` (changes the diff refuses to apply automatically, e.g.
|
|
249
|
+
* enum value removals) are surfaced as `-- NOTE:` comments in UP.
|
|
250
|
+
*
|
|
251
|
+
* Pure and DB-free, so it is unit-testable from a synthesized diff.
|
|
252
|
+
*/
|
|
253
|
+
export function buildDiffMigrationBody(diff) {
|
|
254
|
+
const up = annotateDiffStatements(diff.statements);
|
|
255
|
+
const hasReverse = diff.reverseStatements.length > 0;
|
|
256
|
+
const down = hasReverse
|
|
257
|
+
? annotateDiffStatements(diff.reverseStatements)
|
|
258
|
+
: { text: '', destructive: [] };
|
|
259
|
+
const upParts = [];
|
|
260
|
+
if (up.destructive.length > 0 || down.destructive.length > 0) {
|
|
261
|
+
upParts.push(...DESTRUCTIVE_MIGRATION_BANNER, '');
|
|
262
|
+
}
|
|
263
|
+
if (diff.warnings && diff.warnings.length > 0) {
|
|
264
|
+
for (const w of diff.warnings)
|
|
265
|
+
upParts.push(`-- NOTE: ${w}`);
|
|
266
|
+
upParts.push('');
|
|
267
|
+
}
|
|
268
|
+
upParts.push(up.text || '-- (no statements: schema already matches the database)');
|
|
269
|
+
const downText = hasReverse
|
|
270
|
+
? down.text
|
|
271
|
+
: [
|
|
272
|
+
'-- irreversible, write manually',
|
|
273
|
+
'-- The diff produced no reversible statements for this change. Write the',
|
|
274
|
+
'-- rollback SQL by hand, or leave this section empty for a one-way migration.',
|
|
275
|
+
].join('\n');
|
|
276
|
+
return {
|
|
277
|
+
up: upParts.join('\n'),
|
|
278
|
+
down: downText,
|
|
279
|
+
destructiveUp: up.destructive,
|
|
280
|
+
destructiveDown: down.destructive,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
205
283
|
// ---------------------------------------------------------------------------
|
|
206
284
|
// Commands
|
|
207
285
|
// ---------------------------------------------------------------------------
|
package/dist/client.d.ts
CHANGED
|
@@ -241,6 +241,16 @@ export interface TurbineConfig {
|
|
|
241
241
|
* Default: `true`. Set to `false` as a nuclear kill switch.
|
|
242
242
|
*/
|
|
243
243
|
sqlCache?: boolean;
|
|
244
|
+
/**
|
|
245
|
+
* Maximum number of distinct SQL templates each per-table LRU cache retains.
|
|
246
|
+
*
|
|
247
|
+
* Default: `1000`. Values are parameterized (`$1, $2, …`) and never fragment
|
|
248
|
+
* the cache, so this bounds distinct query SHAPES. Raise it for apps with a
|
|
249
|
+
* very large surface of query shapes to lift the hit rate at the cost of
|
|
250
|
+
* memory; lower it to cap memory. `0` disables caching entirely (identical to
|
|
251
|
+
* `sqlCache: false`); a negative value is ignored (treated as the default).
|
|
252
|
+
*/
|
|
253
|
+
sqlCacheSize?: number;
|
|
244
254
|
/** SQL dialect implementation. Defaults to PostgreSQL. Internal Phase-1 seam for dialect packages. */
|
|
245
255
|
dialect?: Dialect;
|
|
246
256
|
/**
|
package/dist/client.js
CHANGED
|
@@ -366,6 +366,7 @@ export class TurbineClient {
|
|
|
366
366
|
globalFilters: config.globalFilters,
|
|
367
367
|
preparedStatements: envDisablePrepared ? false : (config.preparedStatements ?? !config.pool),
|
|
368
368
|
sqlCache: config.sqlCache ?? true,
|
|
369
|
+
sqlCacheSize: config.sqlCacheSize,
|
|
369
370
|
dialect: config.dialect,
|
|
370
371
|
// Non-SQL backends (PowDB) inject a factory that builds their own query
|
|
371
372
|
// interface (PowqlInterface) instead of the SQL QueryInterface. SQL engines
|
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 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 RelationLoadStrategy, 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';
|
|
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 GroupByResult, 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 OrderByObject, type OrderDirection, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type RelationLoadStrategy, 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 WithOrderByObject, 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,7 +38,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
38
38
|
import { NotFoundError, ReadOnlyError, TimeoutError, UnsupportedFeatureError, ValidationError } from './errors.js';
|
|
39
39
|
import { executeNestedCreate, executeNestedUpdate, hasRelationFields, } from './nested-write.js';
|
|
40
40
|
import { ALL_POWDB_CAPABILITIES, coerceNativeValue, isJsonColumn, isStaleFramePowdbError, PowdbFloatParam, PowdbJsonParam, powqlColumnType, quotePowqlIdent, requireCapability, rowToEntity, } from './powdb.js';
|
|
41
|
-
import { isJsonFilter, isRelationPickOrderBy } from './query/filters.js';
|
|
41
|
+
import { isJsonFilter, isRelationPickOrderBy, orderByEntries } from './query/filters.js';
|
|
42
42
|
import { escapeLike } from './query/utils.js';
|
|
43
43
|
import { normalizeKeyColumns, snakeToCamel, } from './schema.js';
|
|
44
44
|
/**
|
|
@@ -713,7 +713,7 @@ export class PowqlInterface {
|
|
|
713
713
|
buildOrder(orderBy, params, alias) {
|
|
714
714
|
if (!orderBy)
|
|
715
715
|
return '';
|
|
716
|
-
const keys =
|
|
716
|
+
const keys = orderByEntries(orderBy).filter(([, dir]) => dir !== undefined);
|
|
717
717
|
if (!keys.length)
|
|
718
718
|
return '';
|
|
719
719
|
const parts = keys.map(([field, dir]) => {
|
|
@@ -2168,7 +2168,11 @@ export class PowqlInterface {
|
|
|
2168
2168
|
}
|
|
2169
2169
|
const having = this.buildHaving(args.having, params, aggInner);
|
|
2170
2170
|
const order = this.buildGroupOrder(args.orderBy, byOrderExprs, aggOrderExprs);
|
|
2171
|
-
|
|
2171
|
+
// LIMIT / OFFSET over the result groups, applied after ORDER BY (mirrors
|
|
2172
|
+
// the SQL groupBy). offset 0 is a no-op, matching findMany.
|
|
2173
|
+
const limitClause = args.limit !== undefined ? ` limit ${this.param(args.limit, params)}` : '';
|
|
2174
|
+
const offsetClause = args.offset ? ` offset ${this.param(args.offset, params)}` : '';
|
|
2175
|
+
const powql = `${this.qt}${filter} group ${groupExprs.join(', ')}${having}${order}${limitClause}${offsetClause} { ${proj.join(', ')} }`;
|
|
2172
2176
|
const { rows, native: resultNative } = await this.exec(powql, params, args.timeout, 'groupBy');
|
|
2173
2177
|
// Reshape: group keys → user fields (coerced / null-disambiguated),
|
|
2174
2178
|
// aggregates → nested `{ _sum: { field } }`; discriminators are stripped.
|
|
@@ -2274,7 +2278,7 @@ export class PowqlInterface {
|
|
|
2274
2278
|
return keys.join(', ') || '(none)';
|
|
2275
2279
|
};
|
|
2276
2280
|
const parts = [];
|
|
2277
|
-
for (const [key, value] of
|
|
2281
|
+
for (const [key, value] of orderByEntries(orderBy)) {
|
|
2278
2282
|
if (value === undefined)
|
|
2279
2283
|
continue;
|
|
2280
2284
|
if (aggBlocks.has(key)) {
|
|
@@ -25,7 +25,7 @@ export declare function buildGroupBy<T extends object>(qi: BuilderCtx, args: Gro
|
|
|
25
25
|
* no `$n` renumbering). An aggregate key that was not requested, or an unknown
|
|
26
26
|
* by-key, throws {@link ValidationError} E003 listing the valid keys.
|
|
27
27
|
*/
|
|
28
|
-
export declare function buildGroupByOrderBy(qi: BuilderCtx, orderBy: GroupByOrderBy, byOrderExprs: Map<string, string>, aggOrderExprs: Map<string, string>): string;
|
|
28
|
+
export declare function buildGroupByOrderBy(qi: BuilderCtx, orderBy: GroupByOrderBy | GroupByOrderBy[], byOrderExprs: Map<string, string>, aggOrderExprs: Map<string, string>): string;
|
|
29
29
|
/**
|
|
30
30
|
* Validate a JSON-path target (group key or aggregate target) in groupBy:
|
|
31
31
|
* the field must resolve to a real json/jsonb column and the path must be a
|
package/dist/query/aggregates.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { UnsupportedFeatureError, ValidationError } from '../errors.js';
|
|
12
12
|
import { snakeToCamel } from '../schema.js';
|
|
13
|
-
import { isJsonPathOrderBy, isVectorOrderBy, normalizeOrderBy } from './filters.js';
|
|
13
|
+
import { isJsonPathOrderBy, isVectorOrderBy, normalizeOrderBy, orderByEntries } from './filters.js';
|
|
14
14
|
import * as whereMod from './where.js';
|
|
15
15
|
export function buildGroupBy(qi, args) {
|
|
16
16
|
const meta = qi.schema.tables[qi.table];
|
|
@@ -168,6 +168,16 @@ export function buildGroupBy(qi, args) {
|
|
|
168
168
|
if (orderSql)
|
|
169
169
|
sql += ` ORDER BY ${orderSql}`;
|
|
170
170
|
}
|
|
171
|
+
// LIMIT / OFFSET over the result groups, applied AFTER ORDER BY. Routed
|
|
172
|
+
// through the dialect pagination hook (parameterized on PG/SQLite/SQL Server,
|
|
173
|
+
// inlined on MySQL); params append after the WHERE/HAVING/ORDER BY params, so
|
|
174
|
+
// no `$n` renumbering. `offset` without a deterministic `orderBy` yields an
|
|
175
|
+
// arbitrary window (same caveat as findMany).
|
|
176
|
+
if (args.limit !== undefined || args.offset !== undefined) {
|
|
177
|
+
const limitPh = args.limit !== undefined ? qi.paginationRef(args.limit, params) : undefined;
|
|
178
|
+
const offsetPh = args.offset !== undefined ? qi.paginationRef(args.offset, params) : undefined;
|
|
179
|
+
sql += qi.buildPagination(limitPh, offsetPh, args.orderBy !== undefined);
|
|
180
|
+
}
|
|
171
181
|
return {
|
|
172
182
|
sql,
|
|
173
183
|
params,
|
|
@@ -255,7 +265,7 @@ export function buildGroupByOrderBy(qi, orderBy, byOrderExprs, aggOrderExprs) {
|
|
|
255
265
|
return keys.join(', ') || '(none)';
|
|
256
266
|
};
|
|
257
267
|
const parts = [];
|
|
258
|
-
for (const [key, value] of
|
|
268
|
+
for (const [key, value] of orderByEntries(orderBy)) {
|
|
259
269
|
if (value === undefined)
|
|
260
270
|
continue;
|
|
261
271
|
// Aggregate ordering blocks.
|
package/dist/query/builder.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type pg from 'pg';
|
|
14
14
|
import type { SchemaMetadata } from '../schema.js';
|
|
15
|
-
import type { AggregateArgs, AggregateResult, CountArgs, CreateArgs, CreateManyArgs, DeleteArgs, DeleteManyArgs, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GroupByArgs, QueryResult, TypedWithClause, UpdateArgs, UpdateManyArgs, UpsertArgs, WithClause } from './types.js';
|
|
15
|
+
import type { AggregateArgs, AggregateResult, CountArgs, CreateArgs, CreateManyArgs, DeleteArgs, DeleteManyArgs, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GroupByArgs, GroupByResult, QueryResult, TypedWithClause, UpdateArgs, UpdateManyArgs, UpsertArgs, WithClause } from './types.js';
|
|
16
16
|
export type { DeferredQuery, MiddlewareFn, QueryEvent, QueryEventListener, QueryInterfaceOptions, ReselectExecutor, } from './deferred.js';
|
|
17
17
|
import type { DeferredQuery, MiddlewareFn, QueryInterfaceOptions } from './deferred.js';
|
|
18
18
|
export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
@@ -20,7 +20,11 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
20
20
|
private readonly table;
|
|
21
21
|
private readonly schema;
|
|
22
22
|
private readonly tableMeta;
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* SQL template cache: cacheKey → SqlCacheEntry (sql + prepared statement name).
|
|
25
|
+
* Capacity is set once in the constructor from `options.sqlCacheSize`
|
|
26
|
+
* (default 1000). See {@link QueryInterfaceOptions.sqlCacheSize}.
|
|
27
|
+
*/
|
|
24
28
|
private readonly sqlTemplateCache;
|
|
25
29
|
/**
|
|
26
30
|
* Whether the most recent {@link acquireSql} call was a cache HIT. Read by
|
|
@@ -423,7 +427,15 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
|
|
|
423
427
|
}>;
|
|
424
428
|
count(args?: CountArgs<T>): Promise<number>;
|
|
425
429
|
buildCount(args?: CountArgs<T>): DeferredQuery<number>;
|
|
426
|
-
|
|
430
|
+
/**
|
|
431
|
+
* Group rows and compute per-group aggregates (Prisma-style). The result row
|
|
432
|
+
* type is INFERRED from the args: each `by` field carries its entity field
|
|
433
|
+
* type, `_count` is always a number, and each requested `_sum` / `_avg` /
|
|
434
|
+
* `_min` / `_max` block maps its fields to properly typed values (see
|
|
435
|
+
* {@link GroupByResult}). Grouping by a JSON-path key yields a runtime alias
|
|
436
|
+
* that cannot be typed, so those columns are not projected onto the row type.
|
|
437
|
+
*/
|
|
438
|
+
groupBy<A extends GroupByArgs<T>>(args: A): Promise<GroupByResult<T, A>[]>;
|
|
427
439
|
buildGroupBy(args: GroupByArgs<T>): DeferredQuery<Record<string, unknown>[]>;
|
|
428
440
|
buildAggregate(args: AggregateArgs<T>): DeferredQuery<AggregateResult<T>>;
|
|
429
441
|
aggregate(args: AggregateArgs<T>): Promise<AggregateResult<T>>;
|
package/dist/query/builder.js
CHANGED
|
@@ -16,7 +16,7 @@ import { executeNestedCreate, executeNestedUpdate, hasRelationFields, } from '..
|
|
|
16
16
|
import { camelToSnake, snakeToCamel } from '../schema.js';
|
|
17
17
|
import * as aggMod from './aggregates.js';
|
|
18
18
|
import { includeKeysForBatching, loadRelationsBatched, neededParentKeyFields, rejectNestedPickOrder, stripFields, } from './batched-loader.js';
|
|
19
|
-
import { isJsonPathOrderBy, isOrderBySpec, isRelationPickOrderBy, isVectorOrderBy, isWhereOperator, sortedEntries, } from './filters.js';
|
|
19
|
+
import { isJsonPathOrderBy, isOrderBySpec, isRelationPickOrderBy, isVectorOrderBy, isWhereOperator, orderByEntries, sortedEntries, } from './filters.js';
|
|
20
20
|
import * as relationsMod from './relations.js';
|
|
21
21
|
import { LRUCache, ownLookup, parseDbDate, sqlToPreparedName } from './utils.js';
|
|
22
22
|
import * as whereMod from './where.js';
|
|
@@ -149,8 +149,12 @@ export class QueryInterface {
|
|
|
149
149
|
table;
|
|
150
150
|
schema;
|
|
151
151
|
tableMeta;
|
|
152
|
-
/**
|
|
153
|
-
|
|
152
|
+
/**
|
|
153
|
+
* SQL template cache: cacheKey → SqlCacheEntry (sql + prepared statement name).
|
|
154
|
+
* Capacity is set once in the constructor from `options.sqlCacheSize`
|
|
155
|
+
* (default 1000). See {@link QueryInterfaceOptions.sqlCacheSize}.
|
|
156
|
+
*/
|
|
157
|
+
sqlTemplateCache;
|
|
154
158
|
/**
|
|
155
159
|
* Whether the most recent {@link acquireSql} call was a cache HIT. Read by
|
|
156
160
|
* {@link crossCheckCache} to decide whether to run the dev-mode lockstep
|
|
@@ -278,7 +282,13 @@ export class QueryInterface {
|
|
|
278
282
|
: warnOpt !== false;
|
|
279
283
|
this.utcTimestamps = options?.utcTimestamps !== false;
|
|
280
284
|
this.preparedStatementsEnabled = options?.preparedStatements ?? true;
|
|
281
|
-
|
|
285
|
+
// SQL template cache capacity. `sqlCacheSize: 0` disables caching entirely
|
|
286
|
+
// (mirrors `sqlCache: false`); any positive integer sets the LRU bound;
|
|
287
|
+
// undefined keeps the historical 1000-entry default. A negative value is
|
|
288
|
+
// treated as the default rather than throwing.
|
|
289
|
+
const sqlCacheSize = options?.sqlCacheSize;
|
|
290
|
+
this.sqlCacheEnabled = options?.sqlCache !== false && sqlCacheSize !== 0;
|
|
291
|
+
this.sqlTemplateCache = new LRUCache(sqlCacheSize !== undefined && sqlCacheSize > 0 ? Math.floor(sqlCacheSize) : 1000);
|
|
282
292
|
this.dialect = options?.dialect ?? postgresDialect;
|
|
283
293
|
this.relationLoadStrategy = options?.relationLoadStrategy ?? 'join';
|
|
284
294
|
this.jsonEncoding = options?.jsonEncoding ?? 'object';
|
|
@@ -1074,7 +1084,7 @@ export class QueryInterface {
|
|
|
1074
1084
|
// Checked BEFORE the SQL cache so build and warm-cache paths throw
|
|
1075
1085
|
// identically (same rule as the vector guard inside the distinct branch).
|
|
1076
1086
|
if (args?.distinct && args.distinct.length > 0 && args.orderBy) {
|
|
1077
|
-
for (const d of
|
|
1087
|
+
for (const [, d] of orderByEntries(args.orderBy)) {
|
|
1078
1088
|
if (this.isRelationOrderByValue(d)) {
|
|
1079
1089
|
throw new ValidationError('[turbine] `distinct` cannot be combined with relation orderBy (pick-row, `_count`, or ' +
|
|
1080
1090
|
'to-one relation ordering): the outer re-order cannot reference the parent table.');
|
|
@@ -1093,8 +1103,13 @@ export class QueryInterface {
|
|
|
1093
1103
|
// Build fingerprint for cache lookup
|
|
1094
1104
|
const whereFp = hasWhere ? this.fingerprintWhere(whereObj) : '';
|
|
1095
1105
|
const withFp = args?.with ? this.withFingerprint(args.with) : '';
|
|
1106
|
+
// Flatten via orderByEntries so the object form (`{ a, b }`) and the
|
|
1107
|
+
// Prisma-style array form (`[{ a }, { b }]`) fingerprint from the SAME
|
|
1108
|
+
// ordered entry list the build/collect paths consume. An array's element
|
|
1109
|
+
// order is authoritative and preserved here, so a permuted array is a
|
|
1110
|
+
// distinct key (correct, it emits a different ORDER BY).
|
|
1096
1111
|
const orderFp = args?.orderBy
|
|
1097
|
-
?
|
|
1112
|
+
? orderByEntries(args.orderBy)
|
|
1098
1113
|
.map(([k, d]) => `${k}:${this.orderByEntryFingerprint(d, ownLookup(this.tableMeta.relations, k)?.to)}`)
|
|
1099
1114
|
.join(',')
|
|
1100
1115
|
: '';
|
|
@@ -1163,11 +1178,15 @@ export class QueryInterface {
|
|
|
1163
1178
|
// Sorted (canonical) order — MUST match cursorFp and the cache-hit collect below.
|
|
1164
1179
|
const cursorEntries = sortedEntries(args.cursor).filter(([, v]) => v !== undefined);
|
|
1165
1180
|
if (cursorEntries.length > 0) {
|
|
1181
|
+
// Resolve the seek direction per cursor field from the flattened
|
|
1182
|
+
// orderBy entries (last wins, matching object-key semantics), so both
|
|
1183
|
+
// the object and array orderBy forms drive the cursor comparison.
|
|
1184
|
+
const orderDirByKey = new Map(orderByEntries(args.orderBy));
|
|
1166
1185
|
const cursorConditions = cursorEntries.map(([k, v]) => {
|
|
1167
1186
|
const col = this.toSqlColumn(k);
|
|
1168
1187
|
// orderBy values can be the { sort, nulls } spec form: normalize
|
|
1169
1188
|
// before comparing, or a desc spec would seek the ascending side.
|
|
1170
|
-
const dir =
|
|
1189
|
+
const dir = orderDirByKey.get(k);
|
|
1171
1190
|
const desc = isOrderBySpec(dir) ? dir.sort === 'desc' : dir === 'desc';
|
|
1172
1191
|
const op = desc ? '<' : '>';
|
|
1173
1192
|
freshParams.push(v);
|
|
@@ -1188,7 +1207,7 @@ export class QueryInterface {
|
|
|
1188
1207
|
// need two levels: inner DISTINCT ON ordered by the distinct columns then
|
|
1189
1208
|
// the user's order (picks the right representative row), outer re-ordered
|
|
1190
1209
|
// by the user's order alone.
|
|
1191
|
-
if (
|
|
1210
|
+
if (orderByEntries(args.orderBy).some(([, d]) => isVectorOrderBy(d))) {
|
|
1192
1211
|
throw new ValidationError('[turbine] `distinct` cannot be combined with vector distance ordering.');
|
|
1193
1212
|
}
|
|
1194
1213
|
const userOrder = this.buildOrderBy(args.orderBy, freshParams);
|
|
@@ -1602,6 +1621,14 @@ export class QueryInterface {
|
|
|
1602
1621
|
// -------------------------------------------------------------------------
|
|
1603
1622
|
// groupBy (with aggregate functions)
|
|
1604
1623
|
// -------------------------------------------------------------------------
|
|
1624
|
+
/**
|
|
1625
|
+
* Group rows and compute per-group aggregates (Prisma-style). The result row
|
|
1626
|
+
* type is INFERRED from the args: each `by` field carries its entity field
|
|
1627
|
+
* type, `_count` is always a number, and each requested `_sum` / `_avg` /
|
|
1628
|
+
* `_min` / `_max` block maps its fields to properly typed values (see
|
|
1629
|
+
* {@link GroupByResult}). Grouping by a JSON-path key yields a runtime alias
|
|
1630
|
+
* that cannot be typed, so those columns are not projected onto the row type.
|
|
1631
|
+
*/
|
|
1605
1632
|
async groupBy(args) {
|
|
1606
1633
|
return this.executeWithMiddleware('groupBy', args, async () => {
|
|
1607
1634
|
const deferred = this.buildGroupBy(args);
|
|
@@ -1768,7 +1795,7 @@ export class QueryInterface {
|
|
|
1768
1795
|
* order exactly so the cached-SQL param re-collection stays in lockstep.
|
|
1769
1796
|
*/
|
|
1770
1797
|
collectOrderByParams(orderBy, params) {
|
|
1771
|
-
for (const [key, dir] of
|
|
1798
|
+
for (const [key, dir] of orderByEntries(orderBy)) {
|
|
1772
1799
|
if (isVectorOrderBy(dir)) {
|
|
1773
1800
|
const rawColumn = this.toColumn(key);
|
|
1774
1801
|
// Re-run the same validation as buildOrderBy so the collect path can
|
package/dist/query/deferred.d.ts
CHANGED
|
@@ -107,6 +107,17 @@ export interface QueryInterfaceOptions {
|
|
|
107
107
|
* unparseable value keeps the check fully off.
|
|
108
108
|
*/
|
|
109
109
|
sqlCache?: boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Maximum number of distinct SQL templates the per-table LRU cache retains.
|
|
112
|
+
*
|
|
113
|
+
* Default: `1000`. Raise it for apps with a very large number of distinct
|
|
114
|
+
* query SHAPES (not values, since values are parameterized and never fragment the
|
|
115
|
+
* cache) to lift the hit rate at the cost of memory; lower it to cap memory.
|
|
116
|
+
* `0` disables the cache entirely, exactly like `sqlCache: false`. A negative
|
|
117
|
+
* value is ignored (treated as the default). Applied per QueryInterface (one
|
|
118
|
+
* per table).
|
|
119
|
+
*/
|
|
120
|
+
sqlCacheSize?: number;
|
|
110
121
|
/** SQL dialect implementation. Defaults to PostgreSQL. */
|
|
111
122
|
dialect?: Dialect;
|
|
112
123
|
/**
|
package/dist/query/filters.d.ts
CHANGED
|
@@ -173,6 +173,23 @@ export declare function isJsonPathOrderBy(value: unknown): value is JsonPathOrde
|
|
|
173
173
|
* ordering) are excluded up front.
|
|
174
174
|
*/
|
|
175
175
|
export declare function isRelationPickOrderBy(value: unknown): value is RelationPickOrderBy;
|
|
176
|
+
/**
|
|
177
|
+
* Flatten an orderBy input into an ordered list of `[field, value]` entries.
|
|
178
|
+
*
|
|
179
|
+
* Accepts BOTH the classic single-object form (`{ a: 'asc', b: 'desc' }`,
|
|
180
|
+
* whose insertion order is authoritative) and the Prisma-style array form
|
|
181
|
+
* (`[{ a: 'asc' }, { b: 'desc' }]`, whose array order is authoritative). The
|
|
182
|
+
* array form removes the reliance on JS object key iteration order for
|
|
183
|
+
* multi-key sorts. Each array element may carry one or more keys; they expand
|
|
184
|
+
* left-to-right. `undefined`/non-object elements are skipped.
|
|
185
|
+
*
|
|
186
|
+
* Undefined-VALUED entries are preserved (mirroring `Object.entries`) so each
|
|
187
|
+
* consumer keeps its own `dir !== undefined` filtering exactly as before. This
|
|
188
|
+
* is THE single flattening authority: every ORDER BY compile / collect /
|
|
189
|
+
* fingerprint path routes through it so the array and object forms stay in
|
|
190
|
+
* lockstep across build, param-collect, and cache-key fingerprint.
|
|
191
|
+
*/
|
|
192
|
+
export declare function orderByEntries(orderBy: unknown): [string, unknown][];
|
|
176
193
|
/**
|
|
177
194
|
* Normalize an orderBy value into `{ direction, nulls }`. Accepts a plain
|
|
178
195
|
* direction string or an {@link OrderBySpec}. Used by every ORDER BY compile
|
package/dist/query/filters.js
CHANGED
|
@@ -332,6 +332,38 @@ export function isRelationPickOrderBy(value) {
|
|
|
332
332
|
}
|
|
333
333
|
return true;
|
|
334
334
|
}
|
|
335
|
+
/**
|
|
336
|
+
* Flatten an orderBy input into an ordered list of `[field, value]` entries.
|
|
337
|
+
*
|
|
338
|
+
* Accepts BOTH the classic single-object form (`{ a: 'asc', b: 'desc' }`,
|
|
339
|
+
* whose insertion order is authoritative) and the Prisma-style array form
|
|
340
|
+
* (`[{ a: 'asc' }, { b: 'desc' }]`, whose array order is authoritative). The
|
|
341
|
+
* array form removes the reliance on JS object key iteration order for
|
|
342
|
+
* multi-key sorts. Each array element may carry one or more keys; they expand
|
|
343
|
+
* left-to-right. `undefined`/non-object elements are skipped.
|
|
344
|
+
*
|
|
345
|
+
* Undefined-VALUED entries are preserved (mirroring `Object.entries`) so each
|
|
346
|
+
* consumer keeps its own `dir !== undefined` filtering exactly as before. This
|
|
347
|
+
* is THE single flattening authority: every ORDER BY compile / collect /
|
|
348
|
+
* fingerprint path routes through it so the array and object forms stay in
|
|
349
|
+
* lockstep across build, param-collect, and cache-key fingerprint.
|
|
350
|
+
*/
|
|
351
|
+
export function orderByEntries(orderBy) {
|
|
352
|
+
if (Array.isArray(orderBy)) {
|
|
353
|
+
const entries = [];
|
|
354
|
+
for (const element of orderBy) {
|
|
355
|
+
if (element !== null && typeof element === 'object' && !Array.isArray(element)) {
|
|
356
|
+
for (const kv of Object.entries(element))
|
|
357
|
+
entries.push(kv);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return entries;
|
|
361
|
+
}
|
|
362
|
+
if (orderBy !== null && typeof orderBy === 'object') {
|
|
363
|
+
return Object.entries(orderBy);
|
|
364
|
+
}
|
|
365
|
+
return [];
|
|
366
|
+
}
|
|
335
367
|
/**
|
|
336
368
|
* Normalize an orderBy value into `{ direction, nulls }`. Accepts a plain
|
|
337
369
|
* direction string or an {@link OrderBySpec}. Used by every ORDER BY compile
|
package/dist/query/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* `import { … } from './query/index.js'` is a drop-in replacement for the
|
|
6
6
|
* former monolithic `import { … } from './query.js'`.
|
|
7
7
|
*/
|
|
8
|
-
export type { AggregateArgs, AggregateResult, ArrayFilter, ColumnRef, ConnectOrCreateOp, CountArgs, CreateArgs, CreateDataInput, CreateManyArgs, DeleteArgs, DeleteManyArgs, FieldResult, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GlobalFilters, GroupByAggregateSpec, GroupByArgs, GroupByDistinctOn, HavingClause, JsonFilter, JsonPathAggregateTarget, JsonPathGroupKey, JsonPathOrderBy, NestedCreateOp, NestedUpdateOp, NestedUpdateOpItem, NestedUpsertOpItem, OmitResult, OrderByClause, OrderDirection, QueryResult, RelationDescriptor, RelationFilter, RelationLoadStrategy, RelationPickBy, RelationPickOrderBy, SelectResult, SkipGlobalFilters, TextSearchFilter, TypedWithClause, UpdateArgs, UpdateDataInput, UpdateInput, UpdateManyArgs, UpdateOperatorInput, UpsertArgs, VectorDistanceFilter, VectorFilter, VectorMetric, VectorOrderBy, VectorOrderByDistance, WhereClause, WhereOperator, WhereValue, WithClause, WithOptions, WithResult, } from './types.js';
|
|
8
|
+
export type { AggregateArgs, AggregateResult, ArrayFilter, ColumnRef, ConnectOrCreateOp, CountArgs, CreateArgs, CreateDataInput, CreateManyArgs, DeleteArgs, DeleteManyArgs, FieldResult, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GlobalFilters, GroupByAggregateSpec, GroupByArgs, GroupByDistinctOn, GroupByResult, HavingClause, JsonFilter, JsonPathAggregateTarget, JsonPathGroupKey, JsonPathOrderBy, NestedCreateOp, NestedUpdateOp, NestedUpdateOpItem, NestedUpsertOpItem, OmitResult, OrderByClause, OrderByObject, OrderDirection, QueryResult, RelationDescriptor, RelationFilter, RelationLoadStrategy, RelationPickBy, RelationPickOrderBy, SelectResult, SkipGlobalFilters, TextSearchFilter, TypedWithClause, UpdateArgs, UpdateDataInput, UpdateInput, UpdateManyArgs, UpdateOperatorInput, UpsertArgs, VectorDistanceFilter, VectorFilter, VectorMetric, VectorOrderBy, VectorOrderByDistance, WhereClause, WhereOperator, WhereValue, WithClause, WithOptions, WithOrderByObject, WithResult, } from './types.js';
|
|
9
9
|
export type { BuiltStatement, BulkInsertStatementInput, ColumnDefinitionInput, ColumnTypeInput, CreateIndexStatementInput, CreateTableStatementInput, Dialect, InsertStatementInput, UpsertStatementInput, } from '../dialect.js';
|
|
10
10
|
export { postgresDialect } from '../dialect.js';
|
|
11
11
|
export type { SqlCacheEntry } from './utils.js';
|
package/dist/query/relations.js
CHANGED
|
@@ -16,7 +16,7 @@ import { CircularRelationError, RelationError, UnsupportedFeatureError, Validati
|
|
|
16
16
|
import { missingIndexForRelation } from '../index-advisor.js';
|
|
17
17
|
import { camelToSnake, normalizeKeyColumns, snakeToCamel } from '../schema.js';
|
|
18
18
|
import { resolveCountRelations } from './batched-loader.js';
|
|
19
|
-
import { isJsonPathOrderBy, isOrderBySpec, isRelationPickOrderBy, isVectorOrderBy, normalizeOrderBy, sortedEntries, } from './filters.js';
|
|
19
|
+
import { isJsonPathOrderBy, isOrderBySpec, isRelationPickOrderBy, isVectorOrderBy, normalizeOrderBy, orderByEntries, sortedEntries, } from './filters.js';
|
|
20
20
|
import { ownLookup } from './utils.js';
|
|
21
21
|
import * as whereMod from './where.js';
|
|
22
22
|
import * as writesMod from './writes.js';
|
|
@@ -128,7 +128,7 @@ export function withFingerprint(qi, withClause, table, depth = 0) {
|
|
|
128
128
|
// orderBy shape (OrderBySpec nulls placement changes the SQL, so fingerprint it)
|
|
129
129
|
if (opts.orderBy) {
|
|
130
130
|
const targetRels = qi.schema.tables[relDef.to]?.relations;
|
|
131
|
-
const oEntries =
|
|
131
|
+
const oEntries = orderByEntries(opts.orderBy).map(([k, d]) => `${k}:${orderByEntryFingerprint(qi, d, targetRels?.[k]?.to)}`);
|
|
132
132
|
subParts.push(`o=${oEntries.join(',')}`);
|
|
133
133
|
}
|
|
134
134
|
// limit presence, but on inline-pagination engines (MySQL) the literal
|
|
@@ -191,7 +191,7 @@ export function collectRelationSubqueryParams(qi, relDef, spec, params, _parentR
|
|
|
191
191
|
// orderBy params → where params → limit param → nested-with params
|
|
192
192
|
// (always, both paths).
|
|
193
193
|
if (relDef.type === 'manyToMany') {
|
|
194
|
-
const m2mOrderEntries = spec.orderBy ?
|
|
194
|
+
const m2mOrderEntries = spec.orderBy ? orderByEntries(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
|
|
195
195
|
if (nativeOrderPath && m2mOrderEntries.length > 0) {
|
|
196
196
|
collectRelationOrderParams(qi, targetTable, targetMeta, m2mOrderEntries, params);
|
|
197
197
|
}
|
|
@@ -213,7 +213,7 @@ export function collectRelationSubqueryParams(qi, relDef, spec, params, _parentR
|
|
|
213
213
|
return;
|
|
214
214
|
}
|
|
215
215
|
// Mirrors buildRelationSubquery's willWrap: `orderBy: {}` is treated as absent.
|
|
216
|
-
const relOrderEntries = spec.orderBy ?
|
|
216
|
+
const relOrderEntries = spec.orderBy ? orderByEntries(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
|
|
217
217
|
const hasOrder = relOrderEntries.length > 0;
|
|
218
218
|
const willWrap = relDef.type === 'hasMany' && (spec.limit !== undefined || hasOrder);
|
|
219
219
|
// Non-wrapped path: nested relations BEFORE where/limit
|
|
@@ -317,7 +317,7 @@ export function buildOrderBy(qi, orderBy, params, lateralSink) {
|
|
|
317
317
|
// orderBy keys (object values that are neither a vector nor an OrderBySpec)
|
|
318
318
|
// are validated in the relation branch below, so skip them here.
|
|
319
319
|
if (process.env.NODE_ENV !== 'production') {
|
|
320
|
-
for (const [key, value] of
|
|
320
|
+
for (const [key, value] of orderByEntries(orderBy)) {
|
|
321
321
|
if (isRelationOrderByValue(qi, value) && ownLookup(qi.tableMeta.relations, key))
|
|
322
322
|
continue;
|
|
323
323
|
const snakeKey = camelToSnake(key);
|
|
@@ -329,7 +329,7 @@ export function buildOrderBy(qi, orderBy, params, lateralSink) {
|
|
|
329
329
|
}
|
|
330
330
|
const meta = qi.schema.tables[qi.table];
|
|
331
331
|
let relOrdCounter = 0;
|
|
332
|
-
return
|
|
332
|
+
return orderByEntries(orderBy)
|
|
333
333
|
.map(([key, value]) => {
|
|
334
334
|
// Vector KNN ordering: { distance: { to, metric, direction? } }
|
|
335
335
|
if (isVectorOrderBy(value)) {
|
|
@@ -1381,7 +1381,7 @@ export function buildRelationSubquery(qi, relDef, spec, params, parentRef, alias
|
|
|
1381
1381
|
// An orderBy with no defined entries (`orderBy: {}`) is treated as absent —
|
|
1382
1382
|
// it must neither trigger the wrap (dropping nested relations) nor render a
|
|
1383
1383
|
// dangling `ORDER BY `. `limit: 0` is meaningful (LIMIT 0) and DOES wrap.
|
|
1384
|
-
const relOrderEntries = spec !== true && spec.orderBy ?
|
|
1384
|
+
const relOrderEntries = spec !== true && spec.orderBy ? orderByEntries(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
|
|
1385
1385
|
const willWrap = relDef.type === 'hasMany' && spec !== true && (spec.limit !== undefined || relOrderEntries.length > 0);
|
|
1386
1386
|
// manyToMany takes a dedicated JOIN-through-junction path. Nested relations,
|
|
1387
1387
|
// where, orderBy, and select/omit are handled there (the target alias is the
|
|
@@ -1552,7 +1552,7 @@ export function buildManyToManySubquery(qi, relDef, spec, params, parentRef, ali
|
|
|
1552
1552
|
// `orderBy: {}` (no defined entries) is treated as absent: it must not
|
|
1553
1553
|
// render a dangling `ORDER BY `. Param pushes here land BEFORE the
|
|
1554
1554
|
// spec.where params, mirrored by collectRelationSubqueryParams' m2m branch.
|
|
1555
|
-
const relOrderEntries = spec !== true && spec.orderBy ?
|
|
1555
|
+
const relOrderEntries = spec !== true && spec.orderBy ? orderByEntries(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
|
|
1556
1556
|
let orderClause = '';
|
|
1557
1557
|
if (relOrderEntries.length > 0) {
|
|
1558
1558
|
orderClause = buildRelationOrderClause(qi, targetTable, targetMeta, talias, relOrderEntries, params);
|