turbine-orm 0.50.0 → 0.51.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -66
- package/dist/adapters/cockroachdb.d.ts +5 -5
- package/dist/adapters/cockroachdb.js +10 -10
- package/dist/adapters/index.d.ts +5 -5
- package/dist/adapters/index.js +7 -7
- package/dist/adapters/yugabytedb.d.ts +7 -7
- package/dist/adapters/yugabytedb.js +10 -10
- package/dist/cjs/adapters/cockroachdb.d.ts +5 -5
- package/dist/cjs/adapters/cockroachdb.js +10 -10
- package/dist/cjs/adapters/index.d.ts +5 -5
- package/dist/cjs/adapters/index.js +7 -7
- package/dist/cjs/adapters/yugabytedb.d.ts +7 -7
- package/dist/cjs/adapters/yugabytedb.js +10 -10
- package/dist/cjs/cli/config.d.ts +13 -2
- package/dist/cjs/cli/config.js +3 -2
- package/dist/cjs/cli/destructive.d.ts +1 -1
- package/dist/cjs/cli/destructive.js +1 -1
- package/dist/cjs/cli/index.d.ts +10 -10
- package/dist/cjs/cli/index.js +49 -45
- package/dist/cjs/cli/loader.d.ts +7 -7
- package/dist/cjs/cli/loader.js +9 -9
- package/dist/cjs/cli/mcp.js +4 -4
- package/dist/cjs/cli/migrate.d.ts +5 -5
- package/dist/cjs/cli/migrate.js +11 -11
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/ui.d.ts +2 -2
- package/dist/cjs/cli/ui.js +2 -2
- package/dist/cjs/client.d.ts +49 -38
- package/dist/cjs/client.js +57 -56
- package/dist/cjs/dialect.d.ts +62 -18
- package/dist/cjs/dialect.js +40 -2
- package/dist/cjs/errors.d.ts +5 -5
- package/dist/cjs/errors.js +11 -11
- package/dist/cjs/generate.d.ts +6 -6
- package/dist/cjs/generate.js +31 -29
- package/dist/cjs/index-advisor.d.ts +5 -5
- package/dist/cjs/index-advisor.js +0 -0
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js +7 -7
- package/dist/cjs/introspect.d.ts +35 -9
- package/dist/cjs/introspect.js +83 -32
- package/dist/cjs/mssql.d.ts +11 -11
- package/dist/cjs/mssql.js +64 -29
- package/dist/cjs/mysql.d.ts +8 -8
- package/dist/cjs/mysql.js +61 -23
- package/dist/cjs/nested-write.d.ts +21 -2
- package/dist/cjs/nested-write.js +51 -14
- package/dist/cjs/optional-peer-import.cjs +7 -7
- package/dist/cjs/optional-peer-import.d.cts +7 -7
- package/dist/cjs/pipeline-submittable.d.ts +2 -2
- package/dist/cjs/pipeline-submittable.js +6 -6
- package/dist/cjs/pipeline.d.ts +1 -1
- package/dist/cjs/pipeline.js +4 -4
- package/dist/cjs/powdb-introspect.d.ts +1 -1
- package/dist/cjs/powdb-introspect.js +1 -1
- package/dist/cjs/powdb.d.ts +28 -28
- package/dist/cjs/powdb.js +66 -66
- package/dist/cjs/powql.d.ts +27 -27
- package/dist/cjs/powql.js +73 -52
- package/dist/cjs/query/aggregates.d.ts +1 -1
- package/dist/cjs/query/aggregates.js +5 -5
- package/dist/cjs/query/batched-loader.d.ts +11 -11
- package/dist/cjs/query/batched-loader.js +24 -24
- package/dist/cjs/query/builder.d.ts +39 -21
- package/dist/cjs/query/builder.js +99 -57
- package/dist/cjs/query/compound-unique.d.ts +1 -1
- package/dist/cjs/query/compound-unique.js +0 -0
- package/dist/cjs/query/deferred.d.ts +12 -6
- package/dist/cjs/query/deferred.js +1 -1
- package/dist/cjs/query/filters.d.ts +31 -11
- package/dist/cjs/query/filters.js +67 -14
- package/dist/cjs/query/index.d.ts +1 -1
- package/dist/cjs/query/index.js +1 -1
- package/dist/cjs/query/relations.d.ts +9 -9
- package/dist/cjs/query/relations.js +164 -57
- package/dist/cjs/query/types.d.ts +86 -35
- package/dist/cjs/query/types.js +1 -1
- package/dist/cjs/query/utils.d.ts +27 -10
- package/dist/cjs/query/utils.js +86 -14
- package/dist/cjs/query/where.d.ts +47 -28
- package/dist/cjs/query/where.js +130 -31
- package/dist/cjs/query/writes.d.ts +24 -5
- package/dist/cjs/query/writes.js +102 -13
- package/dist/cjs/realtime.d.ts +7 -7
- package/dist/cjs/realtime.js +9 -9
- package/dist/cjs/schema-builder.d.ts +18 -7
- package/dist/cjs/schema-builder.js +17 -10
- package/dist/cjs/schema-metadata.d.ts +3 -3
- package/dist/cjs/schema-metadata.js +9 -9
- package/dist/cjs/schema-sql.d.ts +9 -9
- package/dist/cjs/schema-sql.js +20 -20
- package/dist/cjs/schema.d.ts +19 -9
- package/dist/cjs/schema.js +6 -6
- package/dist/cjs/serverless.d.ts +15 -15
- package/dist/cjs/serverless.js +16 -16
- package/dist/cjs/sqlite.d.ts +8 -8
- package/dist/cjs/sqlite.js +53 -22
- package/dist/cjs/typed-sql.d.ts +4 -4
- package/dist/cjs/typed-sql.js +5 -5
- package/dist/cli/config.d.ts +13 -2
- package/dist/cli/config.js +3 -2
- package/dist/cli/destructive.d.ts +1 -1
- package/dist/cli/destructive.js +1 -1
- package/dist/cli/index.d.ts +10 -10
- package/dist/cli/index.js +49 -45
- package/dist/cli/loader.d.ts +7 -7
- package/dist/cli/loader.js +9 -9
- package/dist/cli/mcp.js +4 -4
- package/dist/cli/migrate.d.ts +5 -5
- package/dist/cli/migrate.js +11 -11
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/ui.d.ts +2 -2
- package/dist/cli/ui.js +2 -2
- package/dist/client.d.ts +49 -38
- package/dist/client.js +57 -56
- package/dist/dialect.d.ts +62 -18
- package/dist/dialect.js +40 -2
- package/dist/errors.d.ts +5 -5
- package/dist/errors.js +11 -11
- package/dist/generate.d.ts +6 -6
- package/dist/generate.js +31 -29
- package/dist/index-advisor.d.ts +5 -5
- package/dist/index-advisor.js +0 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +7 -7
- package/dist/introspect.d.ts +35 -9
- package/dist/introspect.js +82 -32
- package/dist/mssql.d.ts +11 -11
- package/dist/mssql.js +64 -29
- package/dist/mysql.d.ts +8 -8
- package/dist/mysql.js +61 -23
- package/dist/nested-write.d.ts +21 -2
- package/dist/nested-write.js +51 -14
- package/dist/optional-peer-import.cjs +7 -7
- package/dist/optional-peer-import.d.cts +7 -7
- package/dist/pipeline-submittable.d.ts +2 -2
- package/dist/pipeline-submittable.js +6 -6
- package/dist/pipeline.d.ts +1 -1
- package/dist/pipeline.js +4 -4
- package/dist/powdb-introspect.d.ts +1 -1
- package/dist/powdb-introspect.js +1 -1
- package/dist/powdb.d.ts +28 -28
- package/dist/powdb.js +66 -66
- package/dist/powql.d.ts +27 -27
- package/dist/powql.js +73 -52
- package/dist/query/aggregates.d.ts +1 -1
- package/dist/query/aggregates.js +5 -5
- package/dist/query/batched-loader.d.ts +11 -11
- package/dist/query/batched-loader.js +24 -24
- package/dist/query/builder.d.ts +39 -21
- package/dist/query/builder.js +100 -58
- package/dist/query/compound-unique.d.ts +1 -1
- package/dist/query/compound-unique.js +0 -0
- package/dist/query/deferred.d.ts +12 -6
- package/dist/query/deferred.js +1 -1
- package/dist/query/filters.d.ts +31 -11
- package/dist/query/filters.js +66 -13
- package/dist/query/index.d.ts +1 -1
- package/dist/query/index.js +1 -1
- package/dist/query/relations.d.ts +9 -9
- package/dist/query/relations.js +165 -58
- package/dist/query/types.d.ts +86 -35
- package/dist/query/types.js +1 -1
- package/dist/query/utils.d.ts +27 -10
- package/dist/query/utils.js +84 -14
- package/dist/query/where.d.ts +47 -28
- package/dist/query/where.js +129 -32
- package/dist/query/writes.d.ts +24 -5
- package/dist/query/writes.js +101 -13
- package/dist/realtime.d.ts +7 -7
- package/dist/realtime.js +9 -9
- package/dist/schema-builder.d.ts +18 -7
- package/dist/schema-builder.js +17 -10
- package/dist/schema-metadata.d.ts +3 -3
- package/dist/schema-metadata.js +9 -9
- package/dist/schema-sql.d.ts +9 -9
- package/dist/schema-sql.js +20 -20
- package/dist/schema.d.ts +19 -9
- package/dist/schema.js +6 -6
- package/dist/serverless.d.ts +15 -15
- package/dist/serverless.js +16 -16
- package/dist/sqlite.d.ts +8 -8
- package/dist/sqlite.js +53 -22
- package/dist/typed-sql.d.ts +4 -4
- package/dist/typed-sql.js +5 -5
- package/package.json +2 -2
|
@@ -106,7 +106,7 @@ function resolveColumns(qi, select, omit, includePii) {
|
|
|
106
106
|
if (select) {
|
|
107
107
|
// An array here means a caller wrote `select: ['id', 'name']` (Drizzle/SQL
|
|
108
108
|
// style) instead of the object shape. Object.entries() would iterate the
|
|
109
|
-
// numeric indices and throw a cryptic `Unknown field "0"
|
|
109
|
+
// numeric indices and throw a cryptic `Unknown field "0"`, catch it early
|
|
110
110
|
// with an actionable message.
|
|
111
111
|
if (Array.isArray(select)) {
|
|
112
112
|
throw new errors_js_1.ValidationError(`[turbine] "select" must be an object mapping field names to true ` +
|
|
@@ -156,7 +156,7 @@ function withFingerprint(qi, withClause, table, depth = 0) {
|
|
|
156
156
|
const spec = withClause[relName];
|
|
157
157
|
if (!spec)
|
|
158
158
|
continue;
|
|
159
|
-
// Reserved `_count` key
|
|
159
|
+
// Reserved `_count` key, fingerprint by the selected relation set so
|
|
160
160
|
// `_count: true` and `_count: { posts: true }` never share a cache entry.
|
|
161
161
|
if (relName === '_count') {
|
|
162
162
|
const c = spec;
|
|
@@ -242,7 +242,7 @@ function collectWithParams(qi, withClause, params, table, flattenPlan) {
|
|
|
242
242
|
}
|
|
243
243
|
collectRelationSubqueryParams(qi, relDef, relSpec, params, table ?? qi.table);
|
|
244
244
|
}
|
|
245
|
-
// `_count` global-filter params
|
|
245
|
+
// `_count` global-filter params, mirror buildSelectWithRelations, which
|
|
246
246
|
// appends the count subqueries (and any target-filter params) AFTER every
|
|
247
247
|
// relation subquery, in resolveCountRelations order.
|
|
248
248
|
const countSpec = withClause._count;
|
|
@@ -312,14 +312,14 @@ function collectRelationSubqueryParams(qi, relDef, spec, params, _parentRef, dep
|
|
|
312
312
|
if (nativeOrderPath && hasOrder) {
|
|
313
313
|
collectRelationOrderParams(qi, targetTable, targetMeta, relOrderEntries, params);
|
|
314
314
|
}
|
|
315
|
-
// where params
|
|
315
|
+
// where params, mirrors buildAliasWhere push order
|
|
316
316
|
if (spec.where) {
|
|
317
317
|
whereMod.collectAliasWhereParams(qi, targetTable, targetMeta, spec.where, params);
|
|
318
318
|
}
|
|
319
|
-
// Global filter on the target
|
|
319
|
+
// Global filter on the target, mirrors targetGlobalFilterAlias in
|
|
320
320
|
// buildRelationSubquery (pushed after spec.where, before limit).
|
|
321
321
|
whereMod.collectTargetGlobalFilterAlias(qi, targetTable, params);
|
|
322
|
-
// limit param
|
|
322
|
+
// limit param, only hasMany parameterizes its limit (mirrors
|
|
323
323
|
// buildRelationSubquery). belongsTo/hasOne ignore limit (always LIMIT 1), so
|
|
324
324
|
// pushing one here would orphan a param and desync the collect path.
|
|
325
325
|
// `limit: 0` pushes (LIMIT 0 is honored), so check !== undefined.
|
|
@@ -340,7 +340,7 @@ function collectRelationSubqueryParams(qi, relDef, spec, params, _parentRef, dep
|
|
|
340
340
|
* Value-shape fingerprint for a single orderBy entry, so two queries whose
|
|
341
341
|
* ORDER BY differs only in nulls placement, vector metric, or relation-count
|
|
342
342
|
* vs relation-column never collide on one cached SQL string. Captures the
|
|
343
|
-
* SQL-shaping bits (direction, nulls, metric, relation keys)
|
|
343
|
+
* SQL-shaping bits (direction, nulls, metric, relation keys), never values.
|
|
344
344
|
*/
|
|
345
345
|
function orderByEntryFingerprint(qi, d, targetTable) {
|
|
346
346
|
// Vector KNN ordering changes the emitted operator by metric and adds a
|
|
@@ -386,7 +386,7 @@ function orderByEntryFingerprint(qi, d, targetTable) {
|
|
|
386
386
|
// emits one ORDER BY term per entry in Object.entries order, so entry
|
|
387
387
|
// order is SQL-shaping precedence. A sorted fingerprint made
|
|
388
388
|
// `{ name: 'asc', email: 'desc' }` and the swapped literal share one
|
|
389
|
-
// cached SQL string
|
|
389
|
+
// cached SQL string, silently mis-ordered results on a warm cache.
|
|
390
390
|
return `rel(${Object.entries(d)
|
|
391
391
|
.map(([k, v]) => `${k}=${orderByEntryFingerprint(qi, v)}`)
|
|
392
392
|
.join(',')})`;
|
|
@@ -438,7 +438,7 @@ function buildOrderBy(qi, orderBy, params, lateralSink) {
|
|
|
438
438
|
if (isRelationOrderByValue(qi, value)) {
|
|
439
439
|
return buildRelationOrderBy(qi, key, value, `ord${relOrdCounter++}`, params, undefined, lateralSink);
|
|
440
440
|
}
|
|
441
|
-
// Scalar column ordering
|
|
441
|
+
// Scalar column ordering, a plain direction or an OrderBySpec (nulls).
|
|
442
442
|
if (meta && !(key in meta.columnMap)) {
|
|
443
443
|
throw new errors_js_1.ValidationError(`[turbine] Unknown field "${key}" in orderBy on table "${qi.table}". ` +
|
|
444
444
|
`Known fields: ${Object.keys(meta.columnMap).join(', ') || '(none)'}.`);
|
|
@@ -463,7 +463,7 @@ function isRelationOrderByValue(_qi, value) {
|
|
|
463
463
|
}
|
|
464
464
|
/**
|
|
465
465
|
* Render the ` NULLS FIRST` / ` NULLS LAST` suffix for a column ordering.
|
|
466
|
-
* Only PostgreSQL and SQLite support the `NULLS FIRST/LAST` grammar
|
|
466
|
+
* Only PostgreSQL and SQLite support the `NULLS FIRST/LAST` grammar, on any
|
|
467
467
|
* other engine a caller asking for explicit nulls placement gets a clear
|
|
468
468
|
* {@link UnsupportedFeatureError} (E017) instead of broken SQL.
|
|
469
469
|
*/
|
|
@@ -543,6 +543,100 @@ function buildJsonPathOrderEntry(qi, table, meta, field, spec, prefix, params) {
|
|
|
543
543
|
: '';
|
|
544
544
|
return `${lhs} ${dir}${nullsSql}`;
|
|
545
545
|
}
|
|
546
|
+
/**
|
|
547
|
+
* Order by a column reached through TWO OR MORE to-one relation hops, e.g.
|
|
548
|
+
* `orderBy: { model: { category: { name: 'asc' } } }`.
|
|
549
|
+
*
|
|
550
|
+
* One hop already compiled to a correlated scalar subquery; each additional
|
|
551
|
+
* hop is added to that same subquery as an INNER JOIN, so the whole chain is
|
|
552
|
+
* one subquery with one `LIMIT 1` regardless of depth:
|
|
553
|
+
*
|
|
554
|
+
* (SELECT t1."name"
|
|
555
|
+
* FROM "models" t0
|
|
556
|
+
* JOIN "categories" t1 ON t1."id" = t0."category_id"
|
|
557
|
+
* WHERE t0."id" = "versions"."model_id"
|
|
558
|
+
* LIMIT 1) ASC
|
|
559
|
+
*
|
|
560
|
+
* Every hop must be to-one. A to-many hop has no single value to order by, so
|
|
561
|
+
* it is refused rather than silently picking an arbitrary row (`{ pick, by }`
|
|
562
|
+
* exists for that, deliberately, because it forces the caller to say WHICH
|
|
563
|
+
* row). Each hop's target global filter is applied to its join condition, so
|
|
564
|
+
* ordering never keys off a soft-deleted or other-tenant row.
|
|
565
|
+
*/
|
|
566
|
+
function buildChainedToOneOrderBy(qi, head, nextRelName, nextValue, params) {
|
|
567
|
+
const joins = [];
|
|
568
|
+
let currentMeta = qi.schema.tables[head.relDef.to];
|
|
569
|
+
let currentAlias = head.alias;
|
|
570
|
+
let hop = 0;
|
|
571
|
+
let relName = nextRelName;
|
|
572
|
+
let value = nextValue;
|
|
573
|
+
const path = [head.relName];
|
|
574
|
+
// Walk the chain, emitting one JOIN per hop, until the value stops being a
|
|
575
|
+
// relation object. Bounded by the same depth cap as nested `with`.
|
|
576
|
+
for (;;) {
|
|
577
|
+
if (!currentMeta)
|
|
578
|
+
throw new errors_js_1.RelationError(`[turbine] Unknown relation target in orderBy chain "${path.join('.')}"`);
|
|
579
|
+
const relDef = (0, utils_js_1.ownLookup)(currentMeta.relations, relName);
|
|
580
|
+
if (!relDef) {
|
|
581
|
+
throw new errors_js_1.ValidationError(`[turbine] Unknown relation "${relName}" in orderBy on relation "${path.join('.')}" ` +
|
|
582
|
+
`(table "${currentMeta.name}"). Available: ${Object.keys(currentMeta.relations).join(', ') || '(none)'}.`);
|
|
583
|
+
}
|
|
584
|
+
if (relDef.type !== 'belongsTo' && relDef.type !== 'hasOne') {
|
|
585
|
+
throw new errors_js_1.ValidationError(`[turbine] orderBy cannot traverse the to-many relation "${relName}" on "${currentMeta.name}" ` +
|
|
586
|
+
`(path "${path.concat(relName).join('.')}"): a to-many relation has no single value to order by. ` +
|
|
587
|
+
`Use a pick-row ordering ({ pick, by }) at the top level, or order by "_count".`);
|
|
588
|
+
}
|
|
589
|
+
if (++hop > MAX_ORDER_BY_RELATION_HOPS) {
|
|
590
|
+
throw new errors_js_1.CircularRelationError(path.concat(relName));
|
|
591
|
+
}
|
|
592
|
+
const nextMeta = qi.schema.tables[relDef.to];
|
|
593
|
+
if (!nextMeta)
|
|
594
|
+
throw new errors_js_1.RelationError(`[turbine] Unknown relation target "${relDef.to}" in orderBy`);
|
|
595
|
+
const nextAlias = `${head.alias}c${hop}`;
|
|
596
|
+
const on = relDef.type === 'belongsTo'
|
|
597
|
+
? qi.dialect.buildCorrelation(nextAlias, relDef.referenceKey, currentAlias, relDef.foreignKey)
|
|
598
|
+
: qi.dialect.buildCorrelation(nextAlias, relDef.foreignKey, currentAlias, relDef.referenceKey);
|
|
599
|
+
let onSql = on;
|
|
600
|
+
if (params) {
|
|
601
|
+
const gf = whereMod.targetGlobalFilterAlias(qi, relDef.to, nextAlias, params);
|
|
602
|
+
if (gf)
|
|
603
|
+
onSql += ` AND ${gf}`;
|
|
604
|
+
}
|
|
605
|
+
joins.push(`JOIN ${qi.q(relDef.to)} ${nextAlias} ON ${onSql}`);
|
|
606
|
+
path.push(relName);
|
|
607
|
+
currentMeta = nextMeta;
|
|
608
|
+
currentAlias = nextAlias;
|
|
609
|
+
// Exactly one key per level, as with the single-hop form.
|
|
610
|
+
const entries = Object.entries(value);
|
|
611
|
+
if (entries.length !== 1) {
|
|
612
|
+
throw new errors_js_1.ValidationError(`[turbine] orderBy on relation "${path.join('.')}" needs exactly one key per level ` +
|
|
613
|
+
`(got: ${entries.map(([k]) => k).join(', ') || '(empty)'}).`);
|
|
614
|
+
}
|
|
615
|
+
const [key, entryValue] = entries[0];
|
|
616
|
+
if ((0, utils_js_1.ownLookup)(currentMeta.relations, key) && isRelationOrderByValue(qi, entryValue)) {
|
|
617
|
+
relName = key;
|
|
618
|
+
value = entryValue;
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
// Terminal: a column on the last table in the chain.
|
|
622
|
+
const snakeCol = (0, utils_js_1.ownLookup)(currentMeta.columnMap, key) ?? (0, schema_js_1.camelToSnake)(key);
|
|
623
|
+
if (!currentMeta.allColumns.includes(snakeCol)) {
|
|
624
|
+
throw new errors_js_1.ValidationError(`[turbine] Unknown column "${key}" in orderBy on relation "${path.join('.')}" (table "${currentMeta.name}").`);
|
|
625
|
+
}
|
|
626
|
+
const { dir, nulls } = (0, filters_js_1.normalizeOrderBy)(entryValue);
|
|
627
|
+
let where = head.correlation;
|
|
628
|
+
if (params) {
|
|
629
|
+
const gf = whereMod.targetGlobalFilterAlias(qi, head.relDef.to, head.alias, params);
|
|
630
|
+
if (gf)
|
|
631
|
+
where += ` AND ${gf}`;
|
|
632
|
+
}
|
|
633
|
+
const from = `${qi.q(head.relDef.to)} ${head.alias} ${joins.join(' ')}`;
|
|
634
|
+
return (`(SELECT ${currentAlias}.${qi.q(snakeCol)} FROM ${from} WHERE ${where}${qi.limitOneClause()}) ` +
|
|
635
|
+
`${dir}${nullsSuffix(qi, nulls)}`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
/** Depth cap for a chained relation orderBy, mirroring the nested-`with` cap. */
|
|
639
|
+
const MAX_ORDER_BY_RELATION_HOPS = 10;
|
|
546
640
|
/**
|
|
547
641
|
* Compile a relation ordering term. For a to-many relation the only allowed
|
|
548
642
|
* key is `_count`, which becomes a correlated `COUNT(*)` subquery. For a
|
|
@@ -610,14 +704,26 @@ function buildRelationOrderBy(qi, relName, value, alias, params, ctx, lateralSin
|
|
|
610
704
|
}
|
|
611
705
|
return entries
|
|
612
706
|
.map(([col, dirValue]) => {
|
|
707
|
+
// A nested relation: `orderBy: { model: { category: { name: 'asc' } } }`.
|
|
708
|
+
// Each further to-one hop becomes a JOIN inside the SAME correlated
|
|
709
|
+
// subquery rather than another level of nesting, so an N-hop chain still
|
|
710
|
+
// costs one subquery.
|
|
711
|
+
const nestedRel = (0, utils_js_1.ownLookup)(targetMeta.relations, col);
|
|
712
|
+
if (nestedRel && isRelationOrderByValue(qi, dirValue)) {
|
|
713
|
+
return buildChainedToOneOrderBy(qi, { relName, relDef, alias, correlation }, col, dirValue, params);
|
|
714
|
+
}
|
|
613
715
|
// columnMap-first resolution (camelToSnake fallback): mirrors the
|
|
614
716
|
// scalar orderBy path so camelCase-named DB columns resolve here too.
|
|
615
717
|
const snakeCol = (0, utils_js_1.ownLookup)(targetMeta.columnMap, col) ?? (0, schema_js_1.camelToSnake)(col);
|
|
616
718
|
if (!targetMeta.allColumns.includes(snakeCol)) {
|
|
617
|
-
|
|
719
|
+
const relationHint = (0, utils_js_1.ownLookup)(targetMeta.relations, col)
|
|
720
|
+
? ` "${col}" is a relation on "${relDef.to}": order by one of ITS columns, e.g. ` +
|
|
721
|
+
`{ ${relName}: { ${col}: { <column>: 'asc' } } }.`
|
|
722
|
+
: '';
|
|
723
|
+
throw new errors_js_1.ValidationError(`[turbine] Unknown column "${col}" in orderBy on relation "${relName}" (table "${relDef.to}").${relationHint}`);
|
|
618
724
|
}
|
|
619
725
|
const { dir, nulls } = (0, filters_js_1.normalizeOrderBy)(dirValue);
|
|
620
|
-
// Target's global filter applies here too
|
|
726
|
+
// Target's global filter applies here too, otherwise ordering keys off
|
|
621
727
|
// a soft-deleted / other-tenant related row's value (matches the with
|
|
622
728
|
// subquery semantics for belongsTo/hasOne).
|
|
623
729
|
let where = correlation;
|
|
@@ -974,7 +1080,7 @@ function collectManyToManyTargetGlobalFilter(qi, relDef, params) {
|
|
|
974
1080
|
/**
|
|
975
1081
|
* Param-collect mirror of {@link buildRelationCountExpr}'s global-filter
|
|
976
1082
|
* params (hasMany direct filter, or manyToMany EXISTS-on-target). Only pushes
|
|
977
|
-
* when a filter applies
|
|
1083
|
+
* when a filter applies, no-op otherwise.
|
|
978
1084
|
*/
|
|
979
1085
|
function collectRelationCountParams(qi, relDef, params) {
|
|
980
1086
|
if (relDef.type === 'manyToMany') {
|
|
@@ -1128,7 +1234,7 @@ function buildJsonRow(qi, jsonPairs) {
|
|
|
1128
1234
|
// `json_build_object` renders a handful of Postgres types as something other
|
|
1129
1235
|
// than the value the driver hands back for the same column, so the 'join'
|
|
1130
1236
|
// strategy used to disagree with a top-level read, with 'batched', and with
|
|
1131
|
-
// 'flatten'
|
|
1237
|
+
// 'flatten', losing precision outright on numeric and int8. The two halves of
|
|
1132
1238
|
// the fix live here and must stay in lockstep:
|
|
1133
1239
|
//
|
|
1134
1240
|
// SQL side jsonScalarPairs() emits `alias."col"::text` for a divergent
|
|
@@ -1141,9 +1247,17 @@ function buildJsonRow(qi, jsonPairs) {
|
|
|
1141
1247
|
// decode (or vice versa). Postgres-only: other engines neither use
|
|
1142
1248
|
// json_build_object nor share this divergence set.
|
|
1143
1249
|
// ---------------------------------------------------------------------------
|
|
1144
|
-
/**
|
|
1145
|
-
|
|
1146
|
-
|
|
1250
|
+
/**
|
|
1251
|
+
* The JSON-wire rule for one column, or undefined when its JSON rendering
|
|
1252
|
+
* already matches the driver. Routed entirely through the dialect, so each
|
|
1253
|
+
* engine states its own divergence set (see {@link Dialect.jsonWireRule}); a
|
|
1254
|
+
* dialect that omits the hook keeps the pre-0.51 no-cast behavior.
|
|
1255
|
+
*/
|
|
1256
|
+
function jsonWireRuleFor(qi, meta, col) {
|
|
1257
|
+
const type = meta.pgTypes?.[col];
|
|
1258
|
+
if (!type)
|
|
1259
|
+
return undefined;
|
|
1260
|
+
return qi.dialect.jsonWireRule?.(type);
|
|
1147
1261
|
}
|
|
1148
1262
|
/**
|
|
1149
1263
|
* The JSON value expression for one scalar relation column: the plain column
|
|
@@ -1152,9 +1266,7 @@ function usesJsonWireCoercion(qi) {
|
|
|
1152
1266
|
*/
|
|
1153
1267
|
function jsonScalarExpr(qi, targetMeta, col, ref) {
|
|
1154
1268
|
const expr = `${ref}.${qi.q(col)}`;
|
|
1155
|
-
|
|
1156
|
-
return expr;
|
|
1157
|
-
return (0, utils_js_1.jsonWireCoercionOid)(targetMeta.pgTypes?.[col]) === undefined ? expr : `${expr}::text`;
|
|
1269
|
+
return jsonWireRuleFor(qi, targetMeta, col)?.sql(expr) ?? expr;
|
|
1158
1270
|
}
|
|
1159
1271
|
/**
|
|
1160
1272
|
* The `[camelKey, valueExpr]` pairs for a relation's scalar columns. Single
|
|
@@ -1188,16 +1300,13 @@ function jsonWireFields(qi, table, meta) {
|
|
|
1188
1300
|
if (cached !== undefined)
|
|
1189
1301
|
return cached;
|
|
1190
1302
|
let fields = null;
|
|
1191
|
-
const
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
fields = new Map();
|
|
1199
|
-
fields.set(meta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col), oid);
|
|
1200
|
-
}
|
|
1303
|
+
for (const col of meta.allColumns) {
|
|
1304
|
+
const rule = jsonWireRuleFor(qi, meta, col);
|
|
1305
|
+
if (rule === undefined)
|
|
1306
|
+
continue;
|
|
1307
|
+
if (fields === null)
|
|
1308
|
+
fields = new Map();
|
|
1309
|
+
fields.set(meta.reverseColumnMap[col] ?? (0, schema_js_1.snakeToCamel)(col), rule);
|
|
1201
1310
|
}
|
|
1202
1311
|
byTable.set(table, fields);
|
|
1203
1312
|
return fields;
|
|
@@ -1208,8 +1317,6 @@ function jsonWireFields(qi, table, meta) {
|
|
|
1208
1317
|
* so the ordinary relation pays nothing beyond the memo lookup.
|
|
1209
1318
|
*/
|
|
1210
1319
|
function decodeJsonWireRow(qi, row, table, meta) {
|
|
1211
|
-
if (!usesJsonWireCoercion(qi))
|
|
1212
|
-
return row;
|
|
1213
1320
|
const fields = jsonWireFields(qi, table, meta);
|
|
1214
1321
|
if (fields === null)
|
|
1215
1322
|
return row;
|
|
@@ -1218,13 +1325,13 @@ function decodeJsonWireRow(qi, row, table, meta) {
|
|
|
1218
1325
|
// be holding. Measured against an in-place variant on a 20K-child-row join the
|
|
1219
1326
|
// two were indistinguishable, so the copy is free insurance.
|
|
1220
1327
|
let decoded;
|
|
1221
|
-
for (const [field,
|
|
1328
|
+
for (const [field, rule] of fields) {
|
|
1222
1329
|
const raw = row[field];
|
|
1223
1330
|
if (typeof raw !== 'string')
|
|
1224
1331
|
continue;
|
|
1225
1332
|
if (decoded === undefined)
|
|
1226
1333
|
decoded = { ...row };
|
|
1227
|
-
decoded[field] =
|
|
1334
|
+
decoded[field] = rule.decode(raw);
|
|
1228
1335
|
}
|
|
1229
1336
|
return decoded ?? row;
|
|
1230
1337
|
}
|
|
@@ -1293,7 +1400,7 @@ function makeNestedParser(qi, withClause, includePii, flattenPlan) {
|
|
|
1293
1400
|
/**
|
|
1294
1401
|
* Return a shallow copy of a top-level row with each relation column decoded
|
|
1295
1402
|
* from its positional array(s) into the object representation. Only relation
|
|
1296
|
-
* columns are positional
|
|
1403
|
+
* columns are positional, base scalar columns stay object-keyed, so the
|
|
1297
1404
|
* result is exactly what the object encoding would have handed parseNestedRow.
|
|
1298
1405
|
*/
|
|
1299
1406
|
function decodePositionalRelations(qi, row, shapes) {
|
|
@@ -1343,7 +1450,7 @@ function decodePositionalObject(qi, arr, shape) {
|
|
|
1343
1450
|
return obj;
|
|
1344
1451
|
}
|
|
1345
1452
|
// ---------------------------------------------------------------------------
|
|
1346
|
-
// relationLoadStrategy: 'flatten'
|
|
1453
|
+
// relationLoadStrategy: 'flatten', to-one relations compiled as LEFT JOINs
|
|
1347
1454
|
// ---------------------------------------------------------------------------
|
|
1348
1455
|
/**
|
|
1349
1456
|
* Alias prefix for a flattened relation's join. Deliberately distinct from the
|
|
@@ -1358,7 +1465,7 @@ const FLATTEN_ALIAS_PREFIX = 'f';
|
|
|
1358
1465
|
* The top-level `WHERE` and `ORDER BY` reference the parent's columns
|
|
1359
1466
|
* UNQUALIFIED (`WHERE "name" = $1`). A bare `LEFT JOIN "orgs" f0` puts a second
|
|
1360
1467
|
* table in scope, so any column name the two tables share ("id", "name",
|
|
1361
|
-
* "created_at"
|
|
1468
|
+
* "created_at", i.e. most of them) turns those references ambiguous and
|
|
1362
1469
|
* Postgres rejects the statement. Qualifying the parent's references is not an
|
|
1363
1470
|
* option here: they are compiled by the shared WHERE walk, which is scope-blind
|
|
1364
1471
|
* by design.
|
|
@@ -1461,13 +1568,13 @@ function provableUniqueTargetKey(relDef, targetMeta) {
|
|
|
1461
1568
|
*
|
|
1462
1569
|
* A relation is ELIGIBLE when all of the following hold:
|
|
1463
1570
|
* 1. it is `belongsTo` or `hasOne` AND its target-side correlation columns are
|
|
1464
|
-
* provably unique ({@link provableUniqueTargetKey})
|
|
1571
|
+
* provably unique ({@link provableUniqueTargetKey}), the row-multiplication
|
|
1465
1572
|
* guard;
|
|
1466
1573
|
* 2. its spec declares no `limit` and no `orderBy` (both are no-ops over a
|
|
1467
1574
|
* single matching row, but refusing them keeps the emitted SQL and the
|
|
1468
1575
|
* param stream trivially equivalent);
|
|
1469
1576
|
* 3. its nested `with` names only real relations and no reserved `_count`
|
|
1470
|
-
* (nested `_count` is unsupported on every strategy
|
|
1577
|
+
* (nested `_count` is unsupported on every strategy, falling back lets the
|
|
1471
1578
|
* subquery path raise the same error);
|
|
1472
1579
|
* 4. the depth cap is not reached, so a too-deep chain still raises
|
|
1473
1580
|
* {@link CircularRelationError} from the subquery path instead of silently
|
|
@@ -1602,7 +1709,7 @@ function warnFlattenFallback(table, rejects) {
|
|
|
1602
1709
|
* it emits today, down to the cache key).
|
|
1603
1710
|
*
|
|
1604
1711
|
* The plan is a pure function of the schema, the `with` clause shape and
|
|
1605
|
-
* `includePii
|
|
1712
|
+
* `includePii`, never of any bound value, so the build path, the cache-hit
|
|
1606
1713
|
* param-collect path and the row assembler can each recompute it and agree.
|
|
1607
1714
|
*/
|
|
1608
1715
|
function planFlattenWith(qi, table, withClause, includePii) {
|
|
@@ -1905,8 +2012,8 @@ function buildSelectWithRelations(qi, table, withClause, params, columnsList, de
|
|
|
1905
2012
|
const meta = qi.schema.tables[table];
|
|
1906
2013
|
if (!meta)
|
|
1907
2014
|
throw new errors_js_1.ValidationError(`[turbine] Unknown table "${table}"`);
|
|
1908
|
-
// Positional JSON encoding is Postgres-only in v1. Gate here
|
|
1909
|
-
// entry point for every `with` clause
|
|
2015
|
+
// Positional JSON encoding is Postgres-only in v1. Gate here, the single
|
|
2016
|
+
// entry point for every `with` clause, so no engine ever emits the
|
|
1910
2017
|
// json_build_array shape its dialect can't produce (and mssql's FOR JSON
|
|
1911
2018
|
// override path is never reached with positional active).
|
|
1912
2019
|
if (qi.jsonEncoding === 'positional' && qi.dialect.name !== 'postgresql') {
|
|
@@ -2048,7 +2155,7 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
|
|
|
2048
2155
|
const currentDepth = depth ?? 0;
|
|
2049
2156
|
const currentPath = path ?? [qi.table];
|
|
2050
2157
|
const targetTable = relDef.to;
|
|
2051
|
-
// Hard depth cap
|
|
2158
|
+
// Hard depth cap, the `with` clause is a finite JSON structure so users can't
|
|
2052
2159
|
// create true infinite recursion, but extremely deep nesting (10+ levels) produces
|
|
2053
2160
|
// unmanageably large SQL. Back-references (e.g. posts → user → posts) are allowed
|
|
2054
2161
|
// since they are legitimate queries (Prisma supports the same pattern).
|
|
@@ -2073,7 +2180,7 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
|
|
|
2073
2180
|
const miss = (0, index_advisor_js_1.missingIndexForRelation)(qi.schema, relDef);
|
|
2074
2181
|
if (miss && (0, warn_registry_js_1.shouldWarnOnce)(warn_registry_js_1.WARN_NS.unindexedRelation, warnKey)) {
|
|
2075
2182
|
console.warn(`[turbine] Relation "${relDef.name}" on "${relDef.from}" probes ` +
|
|
2076
|
-
`"${miss.table}"(${miss.columns.join(', ')}) which has no covering index
|
|
2183
|
+
`"${miss.table}"(${miss.columns.join(', ')}) which has no covering index, ` +
|
|
2077
2184
|
`each parent row scans the full table. Fix: ${miss.createSql}; ` +
|
|
2078
2185
|
'or run `npx turbine doctor` for a full report.');
|
|
2079
2186
|
}
|
|
@@ -2117,7 +2224,7 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
|
|
|
2117
2224
|
// Determine if this hasMany will take the wrapped subquery path (LIMIT or ORDER BY).
|
|
2118
2225
|
// When wrapping, nested relations are built in the wrapped path referencing innerAlias,
|
|
2119
2226
|
// so we must NOT build them here (they would push orphaned params).
|
|
2120
|
-
// An orderBy with no defined entries (`orderBy: {}`) is treated as absent
|
|
2227
|
+
// An orderBy with no defined entries (`orderBy: {}`) is treated as absent -
|
|
2121
2228
|
// it must neither trigger the wrap (dropping nested relations) nor render a
|
|
2122
2229
|
// dangling `ORDER BY `. `limit: 0` is meaningful (LIMIT 0) and DOES wrap.
|
|
2123
2230
|
const relOrderEntries = spec !== true && spec.orderBy ? (0, filters_js_1.orderByEntries)(spec.orderBy).filter(([, dir]) => dir !== undefined) : [];
|
|
@@ -2128,7 +2235,7 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
|
|
|
2128
2235
|
if (relDef.type === 'manyToMany') {
|
|
2129
2236
|
return buildManyToManySubquery(qi, relDef, spec, params, parentRef, aliasCounter, currentDepth, currentPath, alias, targetMeta, targetColumns, includePii);
|
|
2130
2237
|
}
|
|
2131
|
-
// Nested relations
|
|
2238
|
+
// Nested relations, only in the non-wrapped path (wrapped path builds them separately)
|
|
2132
2239
|
if (!willWrap && spec !== true && spec.with) {
|
|
2133
2240
|
for (const [nestedRelName, nestedSpec] of (0, filters_js_1.sortedEntries)(spec.with)) {
|
|
2134
2241
|
const nestedRelDef = (0, utils_js_1.ownLookup)(targetMeta.relations, nestedRelName);
|
|
@@ -2144,7 +2251,7 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
|
|
|
2144
2251
|
}
|
|
2145
2252
|
}
|
|
2146
2253
|
const jsonObj = buildJsonRow(qi, jsonPairs);
|
|
2147
|
-
// Quote parent ref
|
|
2254
|
+
// Quote parent ref, can be a table name or auto-generated alias
|
|
2148
2255
|
const qParent = qi.q(parentRef);
|
|
2149
2256
|
const qTarget = qi.q(targetTable);
|
|
2150
2257
|
// Build ORDER BY for json_agg: unified with the top-level orderBy surface
|
|
@@ -2155,10 +2262,10 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
|
|
|
2155
2262
|
if (relOrderEntries.length > 0) {
|
|
2156
2263
|
orderClause = buildRelationOrderClause(qi, targetTable, targetMeta, alias, relOrderEntries, params);
|
|
2157
2264
|
}
|
|
2158
|
-
// Build WHERE
|
|
2265
|
+
// Build WHERE, correlate to parent via parentRef (alias or table name).
|
|
2159
2266
|
// For hasMany/hasOne: TARGET has the FK (RelationDef.foreignKey is always
|
|
2160
2267
|
// the child-side column), so alias.fk = parentRef.pk. hasOne is just
|
|
2161
|
-
// hasMany with a unique FK
|
|
2268
|
+
// hasMany with a unique FK, treating it like belongsTo here silently
|
|
2162
2269
|
// correlated the wrong columns (caught dogfooding: uuid = varchar).
|
|
2163
2270
|
// For belongsTo: SOURCE has the FK, so alias.pk = parentRef.fk (reversed).
|
|
2164
2271
|
// Supports composite foreign keys (string[]) via buildCorrelation.
|
|
@@ -2169,20 +2276,20 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
|
|
|
2169
2276
|
else {
|
|
2170
2277
|
whereClause = qi.dialect.buildCorrelation(alias, relDef.foreignKey, qParent, relDef.referenceKey);
|
|
2171
2278
|
}
|
|
2172
|
-
// Additional filters
|
|
2279
|
+
// Additional filters, full scalar where surface (equality, null, operator
|
|
2173
2280
|
// objects, OR/AND/NOT), properly parameterized against this alias.
|
|
2174
2281
|
if (spec !== true && spec.where) {
|
|
2175
2282
|
const extra = whereMod.buildAliasWhere(qi, targetTable, targetMeta, alias, spec.where, params);
|
|
2176
2283
|
if (extra)
|
|
2177
2284
|
whereClause += ` AND ${extra}`;
|
|
2178
2285
|
}
|
|
2179
|
-
// Global filter on the target table (soft-delete / tenancy)
|
|
2286
|
+
// Global filter on the target table (soft-delete / tenancy), AND-merged so
|
|
2180
2287
|
// a `with` never surfaces filtered-out child rows. Pushed AFTER spec.where,
|
|
2181
2288
|
// mirrored by collectRelationSubqueryParams.
|
|
2182
2289
|
const gfExtra = whereMod.targetGlobalFilterAlias(qi, targetTable, alias, params);
|
|
2183
2290
|
if (gfExtra)
|
|
2184
2291
|
whereClause += ` AND ${gfExtra}`;
|
|
2185
|
-
// LIMIT
|
|
2292
|
+
// LIMIT, only meaningful for hasMany. A belongsTo / hasOne subquery returns
|
|
2186
2293
|
// a single row (literal `LIMIT 1` below), so a `spec.limit` here must NOT push
|
|
2187
2294
|
// a parameter: doing so orphans an untyped `$N` that the SQL never references,
|
|
2188
2295
|
// which Postgres rejects with "could not determine data type of parameter $N"
|
|
@@ -2200,7 +2307,7 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
|
|
|
2200
2307
|
// Rewrite: SELECT json_agg(json_build_object(...)) FROM (SELECT * FROM table WHERE ... ORDER BY ... LIMIT N) AS alias
|
|
2201
2308
|
// Inner SELECT always needs all columns for WHERE/ORDER to work; json_build_object filters later
|
|
2202
2309
|
const innerSql = `SELECT ${targetMeta.allColumns.map((c) => `${alias}.${qi.q(c)}`).join(', ')} FROM ${qTarget} ${alias} WHERE ${whereClause}${orderClause}${limitClause}`;
|
|
2203
|
-
// For the json_build_object, reference the inner alias
|
|
2310
|
+
// For the json_build_object, reference the inner alias, only include resolved columns
|
|
2204
2311
|
const innerJsonPairs = jsonScalarPairs(qi, targetMeta, targetColumns, innerAlias);
|
|
2205
2312
|
// Build nested relation subqueries referencing innerAlias
|
|
2206
2313
|
if (spec !== true && spec.with) {
|
|
@@ -2220,7 +2327,7 @@ function buildRelationSubquery(qi, relDef, spec, params, parentRef, aliasCounter
|
|
|
2220
2327
|
}
|
|
2221
2328
|
// Inline ORDER BY only when the dialect's array-agg supports it (PG). For
|
|
2222
2329
|
// hasMany this path is reached only when there is no orderClause, so the
|
|
2223
|
-
// argument is `undefined` either way
|
|
2330
|
+
// argument is `undefined` either way, keeping PG output byte-identical.
|
|
2224
2331
|
const inlineOrder = qi.dialect.aggSupportsInlineOrderBy ? orderClause.trim() || undefined : undefined;
|
|
2225
2332
|
return `SELECT ${qi.dialect.buildJsonArrayAgg(jsonObj, inlineOrder)} FROM ${qTarget} ${alias} WHERE ${whereClause}`;
|
|
2226
2333
|
}
|
|
@@ -2261,7 +2368,7 @@ function buildManyToManySubquery(qi, relDef, spec, params, parentRef, aliasCount
|
|
|
2261
2368
|
// JOIN: junction.targetKey = target.<targetPK>. Composite keys pair positionally.
|
|
2262
2369
|
const targetKeys = (0, schema_js_1.normalizeKeyColumns)(relDef.through.targetKey);
|
|
2263
2370
|
// The target PK is the column(s) the junction's targetKey references. An empty
|
|
2264
|
-
// introspected PK means we cannot know what to JOIN on
|
|
2371
|
+
// introspected PK means we cannot know what to JOIN on, fail loudly rather than
|
|
2265
2372
|
// silently guessing `id` and generating a wrong JOIN.
|
|
2266
2373
|
if (targetMeta.primaryKey.length === 0) {
|
|
2267
2374
|
throw new errors_js_1.ValidationError(`[turbine] manyToMany relation "${relDef.name}" targets table "${targetTable}" which has no primary key; ` +
|
|
@@ -2293,7 +2400,7 @@ function buildManyToManySubquery(qi, relDef, spec, params, parentRef, aliasCount
|
|
|
2293
2400
|
if (relOrderEntries.length > 0) {
|
|
2294
2401
|
orderClause = buildRelationOrderClause(qi, targetTable, targetMeta, talias, relOrderEntries, params);
|
|
2295
2402
|
}
|
|
2296
|
-
// Additional WHERE filters on the target
|
|
2403
|
+
// Additional WHERE filters on the target, full scalar where surface,
|
|
2297
2404
|
// properly parameterized against the target alias.
|
|
2298
2405
|
if (spec !== true && spec.where) {
|
|
2299
2406
|
const extra = whereMod.buildAliasWhere(qi, targetTable, targetMeta, talias, spec.where, params);
|
|
@@ -2305,7 +2412,7 @@ function buildManyToManySubquery(qi, relDef, spec, params, parentRef, aliasCount
|
|
|
2305
2412
|
const gfExtra = whereMod.targetGlobalFilterAlias(qi, targetTable, talias, params);
|
|
2306
2413
|
if (gfExtra)
|
|
2307
2414
|
whereClause += ` AND ${gfExtra}`;
|
|
2308
|
-
// LIMIT
|
|
2415
|
+
// LIMIT, `limit: 0` is honored (LIMIT 0 → empty array)
|
|
2309
2416
|
let limitClause = '';
|
|
2310
2417
|
if (spec !== true && spec.limit !== undefined) {
|
|
2311
2418
|
limitClause = ` LIMIT ${qi.paginationRef(spec.limit, params, 'relation limit')}`;
|