linkgress-orm 0.4.79 → 0.4.81
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/query/grouped-query.d.ts +6 -2
- package/dist/query/grouped-query.d.ts.map +1 -1
- package/dist/query/grouped-query.js +28 -3
- package/dist/query/grouped-query.js.map +1 -1
- package/dist/query/query-builder.d.ts +46 -3
- package/dist/query/query-builder.d.ts.map +1 -1
- package/dist/query/query-builder.js +203 -62
- package/dist/query/query-builder.js.map +1 -1
- package/dist/query/query-utils.d.ts +41 -0
- package/dist/query/query-utils.d.ts.map +1 -1
- package/dist/query/query-utils.js +56 -0
- package/dist/query/query-utils.js.map +1 -1
- package/package.json +80 -80
|
@@ -121,11 +121,22 @@ const MOCK_ROW_CHAIN_ID = Symbol('linkgressMockChainId');
|
|
|
121
121
|
const navigationPathSignature = (path) => path
|
|
122
122
|
.map(step => `${step.alias}:${step.targetTable}:${(step.foreignKeys ?? []).join('+')}:${(step.matches ?? []).join('+')}:${step.isMandatory ? 1 : 0}:${step.sourceAlias ?? ''}`)
|
|
123
123
|
.join('>');
|
|
124
|
-
/**
|
|
125
|
-
|
|
124
|
+
/**
|
|
125
|
+
* A reference mock row: `Object.create(prototype)` plus its own state slots.
|
|
126
|
+
*
|
|
127
|
+
* `chainId` is the identity of the row this navigation hangs off, and it is propagated so the
|
|
128
|
+
* field refs minted from the nav row answer "which query do I belong to?" the same way a plain
|
|
129
|
+
* column ref does. Without it a navigation ref is anonymous, and an outer correlation written
|
|
130
|
+
* THROUGH a navigation (`l.city!.name`) is indistinguishable from the inner table's own
|
|
131
|
+
* navigation of the same name — which is precisely the misbinding isForeignChainRef exists to
|
|
132
|
+
* catch. Undefined stays undefined: rows minted outside a chain (collection mocks) keep the
|
|
133
|
+
* anonymity their own callers rely on.
|
|
134
|
+
*/
|
|
135
|
+
const mintReferenceMockRow = (prototype, chainId) => {
|
|
126
136
|
const mock = Object.create(prototype);
|
|
127
137
|
mock[MOCK_ROW_FIELD_REFS] = {};
|
|
128
138
|
mock[MOCK_ROW_NAV_CACHE] = {};
|
|
139
|
+
mock[MOCK_ROW_CHAIN_ID] = chainId;
|
|
129
140
|
return mock;
|
|
130
141
|
};
|
|
131
142
|
/**
|
|
@@ -272,11 +283,19 @@ const NUMERIC_REGEX = /^-?\d+(\.\d+)?$/;
|
|
|
272
283
|
* Query builder for a table
|
|
273
284
|
*/
|
|
274
285
|
/**
|
|
275
|
-
* Monotonic sequence for query-chain identities. Every root builder gets a
|
|
276
|
-
*
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
286
|
+
* Monotonic sequence for query-chain identities. Every root builder gets a fresh id and
|
|
287
|
+
* derived builders inherit it, so every field ref can say which query it belongs to — which is
|
|
288
|
+
* what lets a subquery tell its OWN refs from ones leaking in from an OUTER chain (see
|
|
289
|
+
* isForeignChainRef).
|
|
290
|
+
*
|
|
291
|
+
* Navigation rows inherit the id of the row they hang off (`mintReferenceMockRow`), so
|
|
292
|
+
* `outer.nav.col` carries the OUTER chain while `inner.nav.col` carries the inner one even
|
|
293
|
+
* when both render under the same alias. That propagation is load-bearing: without it a
|
|
294
|
+
* correlation written through a navigation is anonymous and indistinguishable from the inner
|
|
295
|
+
* table's own navigation of the same name.
|
|
296
|
+
*
|
|
297
|
+
* `CollectionQueryBuilder` deliberately stamps nothing — its refs are anonymous by design, so
|
|
298
|
+
* "carries an id at all" is what marks a correlation on that path.
|
|
280
299
|
*/
|
|
281
300
|
let chainIdSeq = 0;
|
|
282
301
|
class QueryBuilder {
|
|
@@ -507,13 +526,13 @@ class QueryBuilder {
|
|
|
507
526
|
return cachedRow;
|
|
508
527
|
}
|
|
509
528
|
if (holder.prototype !== undefined && mock_row_cache_1.MockRowCache.isEnabled()) {
|
|
510
|
-
return (navCache[relName] = mintReferenceMockRow(holder.prototype));
|
|
529
|
+
return (navCache[relName] = mintReferenceMockRow(holder.prototype, slots[MOCK_ROW_CHAIN_ID]));
|
|
511
530
|
}
|
|
512
531
|
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema, schemaRegistry, // Pass schema registry for nested navigation resolution
|
|
513
532
|
[], // Empty navigation path for first level navigation
|
|
514
533
|
sourceTableName // Pass source table name for lateral join correlation
|
|
515
534
|
);
|
|
516
|
-
return (navCache[relName] = refBuilder.createMockTargetRow(holder));
|
|
535
|
+
return (navCache[relName] = refBuilder.createMockTargetRow(holder, slots[MOCK_ROW_CHAIN_ID]));
|
|
517
536
|
},
|
|
518
537
|
enumerable: false,
|
|
519
538
|
configurable: true,
|
|
@@ -682,7 +701,7 @@ class QueryBuilder {
|
|
|
682
701
|
[], // Empty navigation path for first level navigation
|
|
683
702
|
schema.name // Pass source table name for lateral join correlation
|
|
684
703
|
);
|
|
685
|
-
return (navCache[relName] = refBuilder.createMockTargetRow());
|
|
704
|
+
return (navCache[relName] = refBuilder.createMockTargetRow(undefined, slots[MOCK_ROW_CHAIN_ID]));
|
|
686
705
|
},
|
|
687
706
|
enumerable: false,
|
|
688
707
|
configurable: true,
|
|
@@ -724,6 +743,11 @@ class SelectQueryBuilder {
|
|
|
724
743
|
return schema ? `"${schema}"."${tableName}"` : `"${tableName}"`;
|
|
725
744
|
}
|
|
726
745
|
constructor(schema, client, selector, whereCond, limit, offset, orderBy, executor, manualJoins, joinCounter, isDistinct, schemaRegistry, ctes, collectionStrategy, chainId) {
|
|
746
|
+
/**
|
|
747
|
+
* @internal Aliases the WHERE correlated on, recorded by `detectAndAddJoinsFromCondition`
|
|
748
|
+
* so the shadow check can be re-run after the SELECT list has contributed its joins.
|
|
749
|
+
*/
|
|
750
|
+
this.correlatedAliasesFromCondition = new Set();
|
|
727
751
|
this.orderByFields = [];
|
|
728
752
|
this.manualJoins = [];
|
|
729
753
|
this.joinCounter = 0;
|
|
@@ -1000,7 +1024,7 @@ class SelectQueryBuilder {
|
|
|
1000
1024
|
* .select(g => ({ street: g.key.street, count: g.count() }))
|
|
1001
1025
|
*/
|
|
1002
1026
|
groupBy(selector) {
|
|
1003
|
-
return new grouped_query_1.GroupedQueryBuilder(this.schema, this.client, this.selector, selector, this.whereCond, this.executor, this.manualJoins, this.joinCounter, this.schemaRegistry);
|
|
1027
|
+
return new grouped_query_1.GroupedQueryBuilder(this.schema, this.client, this.selector, selector, this.whereCond, this.executor, this.manualJoins, this.joinCounter, this.schemaRegistry, this.chainId);
|
|
1004
1028
|
}
|
|
1005
1029
|
/**
|
|
1006
1030
|
* Add a LEFT JOIN with a subquery
|
|
@@ -1246,7 +1270,7 @@ class SelectQueryBuilder {
|
|
|
1246
1270
|
[], // Empty navigation path for first level navigation
|
|
1247
1271
|
schema.name // Pass source table name for lateral join correlation
|
|
1248
1272
|
);
|
|
1249
|
-
return (navCache[relName] = refBuilder.createMockTargetRow());
|
|
1273
|
+
return (navCache[relName] = refBuilder.createMockTargetRow(undefined, slots[MOCK_ROW_CHAIN_ID]));
|
|
1250
1274
|
},
|
|
1251
1275
|
enumerable: false,
|
|
1252
1276
|
configurable: true,
|
|
@@ -3711,7 +3735,7 @@ ${joinClauses.join('\n')}`;
|
|
|
3711
3735
|
return cachedRow;
|
|
3712
3736
|
}
|
|
3713
3737
|
if (holder.prototype !== undefined && mock_row_cache_1.MockRowCache.isEnabled()) {
|
|
3714
|
-
return (navCache[relName] = mintReferenceMockRow(holder.prototype));
|
|
3738
|
+
return (navCache[relName] = mintReferenceMockRow(holder.prototype, slots[MOCK_ROW_CHAIN_ID]));
|
|
3715
3739
|
}
|
|
3716
3740
|
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema, // Pass the target schema directly
|
|
3717
3741
|
schemaRegistry, // Pass schema registry for nested resolution
|
|
@@ -3719,7 +3743,7 @@ ${joinClauses.join('\n')}`;
|
|
|
3719
3743
|
sourceTableName // Pass source table name for lateral join correlation
|
|
3720
3744
|
);
|
|
3721
3745
|
// Return a mock object that exposes the target table's columns
|
|
3722
|
-
return (navCache[relName] = refBuilder.createMockTargetRow(holder));
|
|
3746
|
+
return (navCache[relName] = refBuilder.createMockTargetRow(holder, slots[MOCK_ROW_CHAIN_ID]));
|
|
3723
3747
|
},
|
|
3724
3748
|
enumerable: false,
|
|
3725
3749
|
configurable: true,
|
|
@@ -3870,7 +3894,7 @@ ${joinClauses.join('\n')}`;
|
|
|
3870
3894
|
const columnName = nestedValue.__dbColumnName;
|
|
3871
3895
|
// Add JOIN if needed for navigation fields
|
|
3872
3896
|
if (tableAlias !== this.schema.name) {
|
|
3873
|
-
const relConfig = this.
|
|
3897
|
+
const relConfig = this.relationForRef(nestedValue, tableAlias);
|
|
3874
3898
|
if (relConfig && !joins.find(j => j.alias === tableAlias)) {
|
|
3875
3899
|
let targetSchema;
|
|
3876
3900
|
if (relConfig.targetTableBuilder) {
|
|
@@ -4014,7 +4038,7 @@ ${joinClauses.join('\n')}`;
|
|
|
4014
4038
|
const columnName = nestedValue.__dbColumnName;
|
|
4015
4039
|
// Add JOIN if needed for navigation fields
|
|
4016
4040
|
if (tableAlias !== this.schema.name) {
|
|
4017
|
-
const relConfig = this.
|
|
4041
|
+
const relConfig = this.relationForRef(nestedValue, tableAlias);
|
|
4018
4042
|
if (relConfig && !joins.find(j => j.alias === tableAlias)) {
|
|
4019
4043
|
let targetSchema;
|
|
4020
4044
|
if (relConfig.targetTableBuilder) {
|
|
@@ -4065,6 +4089,12 @@ ${joinClauses.join('\n')}`;
|
|
|
4065
4089
|
return;
|
|
4066
4090
|
}
|
|
4067
4091
|
for (const [_key, value] of Object.entries(selection)) {
|
|
4092
|
+
// A ref from another chain is a correlation to an enclosing query, which already has
|
|
4093
|
+
// that table in scope — resolving it here would join a second copy of it into this
|
|
4094
|
+
// subquery. Same rule as the WHERE path; see isForeignChainRef.
|
|
4095
|
+
if ((0, query_utils_2.isForeignChainRef)(value, this.chainId)) {
|
|
4096
|
+
continue;
|
|
4097
|
+
}
|
|
4068
4098
|
if (value && typeof value === 'object' && '__tableAlias' in value && '__dbColumnName' in value) {
|
|
4069
4099
|
// This is a FieldRef with a table alias
|
|
4070
4100
|
const tableAlias = value.__tableAlias;
|
|
@@ -4084,6 +4114,9 @@ ${joinClauses.join('\n')}`;
|
|
|
4084
4114
|
// SqlFragment may contain navigation property references
|
|
4085
4115
|
const fieldRefs = value.getFieldRefs();
|
|
4086
4116
|
for (const fieldRef of fieldRefs) {
|
|
4117
|
+
if ((0, query_utils_2.isForeignChainRef)(fieldRef, this.chainId)) {
|
|
4118
|
+
continue;
|
|
4119
|
+
}
|
|
4087
4120
|
if ('__tableAlias' in fieldRef && fieldRef.__tableAlias) {
|
|
4088
4121
|
const tableAlias = fieldRef.__tableAlias;
|
|
4089
4122
|
if (tableAlias && tableAlias !== this.schema.name) {
|
|
@@ -4106,6 +4139,25 @@ ${joinClauses.join('\n')}`;
|
|
|
4106
4139
|
}
|
|
4107
4140
|
}
|
|
4108
4141
|
}
|
|
4142
|
+
/**
|
|
4143
|
+
* The relation `ref` navigates, or `undefined` when `ref` does not belong to this query.
|
|
4144
|
+
*
|
|
4145
|
+
* Every inline "field ref -> relation -> JOIN" site routes through here instead of reading
|
|
4146
|
+
* `this.schema.relations[alias]` directly, because the alias alone cannot tell a navigation
|
|
4147
|
+
* of OURS from a correlation to an enclosing query: a relation name may equal an outer
|
|
4148
|
+
* table's alias (a child's `library` navigation against a parent table also called
|
|
4149
|
+
* `library`). Joining on that name pulls a second copy of the outer table into this query
|
|
4150
|
+
* and rebinds the correlation to it — the predicate then compares the inner row with
|
|
4151
|
+
* itself and is true for every row, with no SQL error and no type error to show for it.
|
|
4152
|
+
*
|
|
4153
|
+
* See isForeignChainRef for how the two are told apart.
|
|
4154
|
+
*/
|
|
4155
|
+
relationForRef(ref, tableAlias) {
|
|
4156
|
+
if ((0, query_utils_2.isForeignChainRef)(ref, this.chainId)) {
|
|
4157
|
+
return undefined;
|
|
4158
|
+
}
|
|
4159
|
+
return this.schema.relations[tableAlias];
|
|
4160
|
+
}
|
|
4109
4161
|
/**
|
|
4110
4162
|
* Resolve all navigation joins by finding the correct path through the schema graph
|
|
4111
4163
|
* This handles multi-level navigation like task.level.createdBy
|
|
@@ -4180,7 +4232,7 @@ ${joinClauses.join('\n')}`;
|
|
|
4180
4232
|
const tableAlias = fieldRef.__tableAlias;
|
|
4181
4233
|
if (tableAlias && tableAlias !== this.schema.name && !joins.some(j => j.alias === tableAlias)) {
|
|
4182
4234
|
// This references a related table - find the relation and add a JOIN
|
|
4183
|
-
const relation = this.
|
|
4235
|
+
const relation = this.relationForRef(fieldRef, tableAlias);
|
|
4184
4236
|
if (relation && relation.type === 'one') {
|
|
4185
4237
|
// Get target schema from targetTableBuilder if available
|
|
4186
4238
|
let targetSchema;
|
|
@@ -4209,8 +4261,19 @@ ${joinClauses.join('\n')}`;
|
|
|
4209
4261
|
}
|
|
4210
4262
|
// Collect all table aliases from the condition
|
|
4211
4263
|
const allTableAliases = new Set();
|
|
4264
|
+
const correlatedAliases = new Set();
|
|
4212
4265
|
const fieldRefs = condition.getFieldRefs();
|
|
4213
4266
|
for (const fieldRef of fieldRefs) {
|
|
4267
|
+
// A ref minted by a DIFFERENT chain belongs to an enclosing query: it is a
|
|
4268
|
+
// CORRELATION, not one of our navigations, and the outer query already has that
|
|
4269
|
+
// table in scope. Joining it here would resolve the alias against OUR relations and
|
|
4270
|
+
// pull in a second, inner copy of the outer table — see isForeignChainRef.
|
|
4271
|
+
if ((0, query_utils_2.isForeignChainRef)(fieldRef, this.chainId)) {
|
|
4272
|
+
if ('__tableAlias' in fieldRef && fieldRef.__tableAlias) {
|
|
4273
|
+
correlatedAliases.add(fieldRef.__tableAlias);
|
|
4274
|
+
}
|
|
4275
|
+
continue;
|
|
4276
|
+
}
|
|
4214
4277
|
if ('__tableAlias' in fieldRef && fieldRef.__tableAlias) {
|
|
4215
4278
|
const tableAlias = fieldRef.__tableAlias;
|
|
4216
4279
|
if (tableAlias !== this.schema.name) {
|
|
@@ -4227,6 +4290,11 @@ ${joinClauses.join('\n')}`;
|
|
|
4227
4290
|
}
|
|
4228
4291
|
}
|
|
4229
4292
|
}
|
|
4293
|
+
// Kept for the second, wider check once the SELECT list has added its own joins: the
|
|
4294
|
+
// colliding navigation can be named ONLY in the projection, which this method never sees.
|
|
4295
|
+
this.correlatedAliasesFromCondition = correlatedAliases;
|
|
4296
|
+
// Refuse the one shape that cannot be rendered — see assertNoCorrelatedAliasShadowing.
|
|
4297
|
+
(0, query_utils_2.assertNoCorrelatedAliasShadowing)(this.schema.name, correlatedAliases, allTableAliases);
|
|
4230
4298
|
// Resolve all joins through the schema graph
|
|
4231
4299
|
this.resolveJoinsForTableAliases(allTableAliases, joins);
|
|
4232
4300
|
}
|
|
@@ -4247,6 +4315,10 @@ ${joinClauses.join('\n')}`;
|
|
|
4247
4315
|
this.detectAndAddJoinsFromSelection(selection, joins);
|
|
4248
4316
|
// Scan WHERE condition for navigation property references and add JOINs
|
|
4249
4317
|
this.detectAndAddJoinsFromCondition(this.whereCond, joins);
|
|
4318
|
+
// Repeat the shadow check now that the SELECT list has contributed its joins: the colliding
|
|
4319
|
+
// navigation can be named ONLY in the projection, where the WHERE-time check cannot see it.
|
|
4320
|
+
// EXISTS ignores the select list, so nothing else would catch that shape.
|
|
4321
|
+
(0, query_utils_2.assertNoCorrelatedAliasShadowing)(this.schema.name, this.correlatedAliasesFromCondition, new Set(joins.map(join => join.alias)));
|
|
4250
4322
|
// Handle case where selection is a single value (not an object with properties)
|
|
4251
4323
|
if (selection instanceof conditions_1.SqlFragment) {
|
|
4252
4324
|
// Single SQL fragment - just build it directly
|
|
@@ -4317,7 +4389,7 @@ ${joinClauses.join('\n')}`;
|
|
|
4317
4389
|
const tableAlias = value.__tableAlias;
|
|
4318
4390
|
const columnName = value.__dbColumnName;
|
|
4319
4391
|
// Find the relation config for this navigation
|
|
4320
|
-
const relConfig = this.
|
|
4392
|
+
const relConfig = this.relationForRef(value, tableAlias);
|
|
4321
4393
|
if (relConfig) {
|
|
4322
4394
|
// Add JOIN if not already added
|
|
4323
4395
|
if (!joins.find(j => j.alias === tableAlias)) {
|
|
@@ -4419,7 +4491,7 @@ ${joinClauses.join('\n')}`;
|
|
|
4419
4491
|
const firstValue = value[tableAlias];
|
|
4420
4492
|
if (firstValue && typeof firstValue === 'object' && '__tableAlias' in firstValue) {
|
|
4421
4493
|
const alias = firstValue.__tableAlias;
|
|
4422
|
-
const relConfig = this.
|
|
4494
|
+
const relConfig = this.relationForRef(firstValue, alias);
|
|
4423
4495
|
if (relConfig && relConfig.type === 'one') {
|
|
4424
4496
|
// This is a reference navigation - select all fields from the target table
|
|
4425
4497
|
// Performance: Use cached target schema
|
|
@@ -4717,6 +4789,10 @@ ${joinClauses.join('\n')}`;
|
|
|
4717
4789
|
this.detectAndAddJoinsFromSelection(selection, joins);
|
|
4718
4790
|
// Scan WHERE condition for navigation property references and add JOINs
|
|
4719
4791
|
this.detectAndAddJoinsFromCondition(this.whereCond, joins);
|
|
4792
|
+
// Repeat the shadow check now that the SELECT list has contributed its joins: the colliding
|
|
4793
|
+
// navigation can be named ONLY in the projection, where the WHERE-time check cannot see it.
|
|
4794
|
+
// EXISTS ignores the select list, so nothing else would catch that shape.
|
|
4795
|
+
(0, query_utils_2.assertNoCorrelatedAliasShadowing)(this.schema.name, this.correlatedAliasesFromCondition, new Set(joins.map(join => join.alias)));
|
|
4720
4796
|
// Handle case where selection is a single value (not an object with properties)
|
|
4721
4797
|
if (selection instanceof conditions_1.SqlFragment) {
|
|
4722
4798
|
const sqlBuildContext = {
|
|
@@ -4791,7 +4867,7 @@ ${joinClauses.join('\n')}`;
|
|
|
4791
4867
|
if ('__tableAlias' in value && value.__tableAlias && typeof value.__tableAlias === 'string') {
|
|
4792
4868
|
const tableAlias = value.__tableAlias;
|
|
4793
4869
|
const columnName = value.__dbColumnName;
|
|
4794
|
-
const relConfig = this.
|
|
4870
|
+
const relConfig = this.relationForRef(value, tableAlias);
|
|
4795
4871
|
if (relConfig && !joins.find(j => j.alias === tableAlias)) {
|
|
4796
4872
|
let targetSchema;
|
|
4797
4873
|
if (relConfig.targetTableBuilder) {
|
|
@@ -5816,7 +5892,15 @@ ${joinClauses.join('\n')}`;
|
|
|
5816
5892
|
// If the table alias doesn't match our current schema, it's from an outer query
|
|
5817
5893
|
// Also check if it's not a navigation property of this table (which would be in schema.relations)
|
|
5818
5894
|
// and not one of our own manual-join aliases (filter-joins qualify refs by join alias).
|
|
5819
|
-
|
|
5895
|
+
//
|
|
5896
|
+
// Chain identity outranks both name checks: a ref stamped by another chain is a
|
|
5897
|
+
// correlation whatever it is called, and reading it as our own navigation (because a
|
|
5898
|
+
// relation happens to carry the same name) is what silently misbinds the predicate.
|
|
5899
|
+
// See isForeignChainRef.
|
|
5900
|
+
if (tableAlias !== currentTableName && (0, query_utils_2.isForeignChainRef)(ref, this.chainId)) {
|
|
5901
|
+
outerRefs.push(ref);
|
|
5902
|
+
}
|
|
5903
|
+
else if (tableAlias !== currentTableName && !this.schema.relations[tableAlias]) {
|
|
5820
5904
|
if (!this.manualJoins.some(j => j.alias === tableAlias)) {
|
|
5821
5905
|
outerRefs.push(ref);
|
|
5822
5906
|
}
|
|
@@ -5901,7 +5985,7 @@ class ReferenceQueryBuilder {
|
|
|
5901
5985
|
* Create a mock object that exposes the target table's columns
|
|
5902
5986
|
* This allows accessing related fields like: p.user.username
|
|
5903
5987
|
*/
|
|
5904
|
-
createMockTargetRow(holder) {
|
|
5988
|
+
createMockTargetRow(holder, chainId) {
|
|
5905
5989
|
if (this.targetTableSchema) {
|
|
5906
5990
|
// Prototype-level cache — see MockRowCache's doc. Everything the getters close over
|
|
5907
5991
|
// is fully determined by (target schema object identity, relationName, sourceAlias,
|
|
@@ -5928,7 +6012,7 @@ class ReferenceQueryBuilder {
|
|
|
5928
6012
|
holder.prototype = prototype;
|
|
5929
6013
|
}
|
|
5930
6014
|
}
|
|
5931
|
-
return mintReferenceMockRow(prototype);
|
|
6015
|
+
return mintReferenceMockRow(prototype, chainId);
|
|
5932
6016
|
}
|
|
5933
6017
|
else {
|
|
5934
6018
|
// Fallback: use the shared nested proxy that supports deep property access
|
|
@@ -5972,6 +6056,8 @@ class ReferenceQueryBuilder {
|
|
|
5972
6056
|
__fieldName: colName,
|
|
5973
6057
|
__dbColumnName: dbColumnName,
|
|
5974
6058
|
__tableAlias: tableAlias, // Alias for SQL generation
|
|
6059
|
+
// Identity of the query this navigation hangs off — see mintReferenceMockRow.
|
|
6060
|
+
__chainId: slots[MOCK_ROW_CHAIN_ID],
|
|
5975
6061
|
__sourceTable: sourceTable, // Actual table name for mapper lookup
|
|
5976
6062
|
__mapper: mapper, // Include mapper for toDriver transformation in conditions
|
|
5977
6063
|
__sqlType: columnSqlTypes[colName], // Column SQL type — lets flag* emit width-exact mask casts
|
|
@@ -6055,7 +6141,7 @@ class ReferenceQueryBuilder {
|
|
|
6055
6141
|
let cached = navCache[relName];
|
|
6056
6142
|
if (cached === undefined) {
|
|
6057
6143
|
if (holder.prototype !== undefined && mock_row_cache_1.MockRowCache.isEnabled()) {
|
|
6058
|
-
cached = navCache[relName] = mintReferenceMockRow(holder.prototype);
|
|
6144
|
+
cached = navCache[relName] = mintReferenceMockRow(holder.prototype, slots[MOCK_ROW_CHAIN_ID]);
|
|
6059
6145
|
}
|
|
6060
6146
|
else {
|
|
6061
6147
|
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, nestedTargetSchema, // Pass the target schema directly
|
|
@@ -6063,7 +6149,7 @@ class ReferenceQueryBuilder {
|
|
|
6063
6149
|
extendedNavPath, // Pass navigation path for nested collections
|
|
6064
6150
|
parentSourceAlias ? tableAlias : '' // Only set source if tracking path
|
|
6065
6151
|
);
|
|
6066
|
-
cached = navCache[relName] = refBuilder.createMockTargetRow(holder);
|
|
6152
|
+
cached = navCache[relName] = refBuilder.createMockTargetRow(holder, slots[MOCK_ROW_CHAIN_ID]);
|
|
6067
6153
|
}
|
|
6068
6154
|
}
|
|
6069
6155
|
return cached;
|
|
@@ -6258,14 +6344,14 @@ class CollectionQueryBuilder {
|
|
|
6258
6344
|
return cachedRow;
|
|
6259
6345
|
}
|
|
6260
6346
|
if (holder.prototype !== undefined && mock_row_cache_1.MockRowCache.isEnabled()) {
|
|
6261
|
-
return (navCache[relName] = mintReferenceMockRow(holder.prototype));
|
|
6347
|
+
return (navCache[relName] = mintReferenceMockRow(holder.prototype, slots[MOCK_ROW_CHAIN_ID]));
|
|
6262
6348
|
}
|
|
6263
6349
|
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, undefined, // Don't pass schema, force registry lookup
|
|
6264
6350
|
schemaRegistry, // Pass schema registry for nested resolution
|
|
6265
6351
|
[], // Empty navigation path - this is the first reference in the chain
|
|
6266
6352
|
targetTable // Source alias is this collection's target table
|
|
6267
6353
|
);
|
|
6268
|
-
return (navCache[relName] = refBuilder.createMockTargetRow(holder));
|
|
6354
|
+
return (navCache[relName] = refBuilder.createMockTargetRow(holder, slots[MOCK_ROW_CHAIN_ID]));
|
|
6269
6355
|
},
|
|
6270
6356
|
enumerable: false,
|
|
6271
6357
|
configurable: true,
|
|
@@ -6497,11 +6583,22 @@ class CollectionQueryBuilder {
|
|
|
6497
6583
|
}
|
|
6498
6584
|
/**
|
|
6499
6585
|
* Get field references from this condition.
|
|
6500
|
-
*
|
|
6501
|
-
*
|
|
6586
|
+
*
|
|
6587
|
+
* Only the ones belonging to an ENCLOSING query are surfaced. Those are correlations, and
|
|
6588
|
+
* the outer query has to have their tables in scope for the subquery to reference them: a
|
|
6589
|
+
* lambda saying `l.city!.name` needs the OUTER query to join `city`, or the emitted
|
|
6590
|
+
* `"city"."name"` has no FROM-clause entry to bind to. Reporting them here is what lets the
|
|
6591
|
+
* parent's join detection see the requirement.
|
|
6592
|
+
*
|
|
6593
|
+
* Our OWN refs stay hidden, as they always were — they are emitted inside this subquery and
|
|
6594
|
+
* must not drag joins into the parent. Required for duck-typing compatibility with the
|
|
6595
|
+
* Condition interface when used in WHERE clauses.
|
|
6502
6596
|
*/
|
|
6503
6597
|
getFieldRefs() {
|
|
6504
|
-
|
|
6598
|
+
if (!this.whereCond) {
|
|
6599
|
+
return [];
|
|
6600
|
+
}
|
|
6601
|
+
return this.whereCond.getFieldRefs().filter(ref => (0, query_utils_2.isForeignChainRef)(ref, undefined));
|
|
6505
6602
|
}
|
|
6506
6603
|
/**
|
|
6507
6604
|
* Build SQL for this collection as a correlated EXISTS subquery.
|
|
@@ -6548,38 +6645,11 @@ class CollectionQueryBuilder {
|
|
|
6548
6645
|
// the statement failed with `missing FROM-clause entry for table "<alias>"`.
|
|
6549
6646
|
// Reuses the selector path's machinery: seed aliases + chains from the
|
|
6550
6647
|
// condition's FieldRefs, then resolve multi-hop paths against the target schema.
|
|
6551
|
-
|
|
6552
|
-
const
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
const targetSchema = this.schemaRegistry?.get(targetTable);
|
|
6557
|
-
if (targetSchema) {
|
|
6558
|
-
const whereJoins = [];
|
|
6559
|
-
const whereAliases = new Set();
|
|
6560
|
-
for (const ref of this.whereCond.getFieldRefs()) {
|
|
6561
|
-
const refAlias = ref?.__tableAlias;
|
|
6562
|
-
// Direct columns of the target arrive under the `__collection_<table>__`
|
|
6563
|
-
// marker (rewritten below) or the bare table name — neither needs a join.
|
|
6564
|
-
if (!refAlias || refAlias === targetTable || refAlias.startsWith('__collection_') || joinedAliases.has(refAlias)) {
|
|
6565
|
-
continue;
|
|
6566
|
-
}
|
|
6567
|
-
this.addNavigationJoinForFieldRef(ref, whereJoins, targetTable, targetSchema, whereAliases);
|
|
6568
|
-
}
|
|
6569
|
-
if (whereAliases.size > 0) {
|
|
6570
|
-
this.resolveNavigationJoins(whereAliases, whereJoins, targetSchema);
|
|
6571
|
-
}
|
|
6572
|
-
for (const nav of whereJoins) {
|
|
6573
|
-
if (joinedAliases.has(nav.alias)) {
|
|
6574
|
-
continue;
|
|
6575
|
-
}
|
|
6576
|
-
joinedAliases.add(nav.alias);
|
|
6577
|
-
const joinType = nav.isMandatory ? 'JOIN' : 'LEFT JOIN';
|
|
6578
|
-
const fk = nav.foreignKeys[0];
|
|
6579
|
-
const pk = (nav.matches && nav.matches.length > 0) ? nav.matches[0] : 'id';
|
|
6580
|
-
allJoins.push(`${joinType} "${nav.targetTable}" "${nav.alias}" ON "${nav.sourceAlias}"."${fk}" = "${nav.alias}"."${pk}"`);
|
|
6581
|
-
}
|
|
6582
|
-
}
|
|
6648
|
+
for (const nav of this.resolveWhereNavigationJoins(sourceTable)) {
|
|
6649
|
+
const joinType = nav.isMandatory ? 'JOIN' : 'LEFT JOIN';
|
|
6650
|
+
const fk = nav.foreignKeys[0];
|
|
6651
|
+
const pk = (nav.matches && nav.matches.length > 0) ? nav.matches[0] : 'id';
|
|
6652
|
+
allJoins.push(`${joinType} "${nav.targetTable}" "${nav.alias}" ON "${nav.sourceAlias}"."${fk}" = "${nav.alias}"."${pk}"`);
|
|
6583
6653
|
}
|
|
6584
6654
|
const navJoinsSQL = allJoins.join('\n');
|
|
6585
6655
|
// Build WHERE clause: correlation + additional conditions
|
|
@@ -6693,10 +6763,73 @@ class CollectionQueryBuilder {
|
|
|
6693
6763
|
* Add a navigation JOIN for a FieldRef if it references a related table
|
|
6694
6764
|
* Handles multi-level navigation by recursively resolving the join chain
|
|
6695
6765
|
*/
|
|
6766
|
+
/**
|
|
6767
|
+
* The joins required by REFERENCE navigations inside this collection's OWN where-condition,
|
|
6768
|
+
* e.g. `shelves.where(s => eq(s.city!.name, 'Rural'))`.
|
|
6769
|
+
*
|
|
6770
|
+
* Navigation joins used to be derived from SELECTORS only, which left a where-navigated
|
|
6771
|
+
* alias unjoined. Both collection render paths need this and neither can rely on the other:
|
|
6772
|
+
* an `exists()` aggregation has no selector at all, and a collection in a PROJECTION has one
|
|
6773
|
+
* that says nothing about the where-clause. Emitting it in one place is what keeps the two
|
|
6774
|
+
* paths answering the same question — the projection path previously bound such an alias to
|
|
6775
|
+
* whatever the OUTER query happened to have joined under that name (silently wrong under
|
|
6776
|
+
* `lateral`) or failed with `missing FROM-clause entry` when it had not.
|
|
6777
|
+
*
|
|
6778
|
+
* `correlationAlias` is the parent's alias in the collection's implicit correlation; a
|
|
6779
|
+
* navigation of ours named the same would shadow it, which cannot be rendered.
|
|
6780
|
+
*/
|
|
6781
|
+
resolveWhereNavigationJoins(correlationAlias) {
|
|
6782
|
+
if (!this.whereCond) {
|
|
6783
|
+
return [];
|
|
6784
|
+
}
|
|
6785
|
+
const targetSchema = this.schemaRegistry?.get(this.targetTable);
|
|
6786
|
+
if (!targetSchema) {
|
|
6787
|
+
return [];
|
|
6788
|
+
}
|
|
6789
|
+
const alreadyJoined = new Set([
|
|
6790
|
+
...this.navigationPath.map(nav => nav.alias),
|
|
6791
|
+
...this.selectManyJoins.map(nav => nav.alias),
|
|
6792
|
+
]);
|
|
6793
|
+
const whereJoins = [];
|
|
6794
|
+
const whereAliases = new Set();
|
|
6795
|
+
for (const ref of this.whereCond.getFieldRefs()) {
|
|
6796
|
+
const refAlias = ref?.__tableAlias;
|
|
6797
|
+
// Correlations to the enclosing row are filtered inside `addNavigationJoinForFieldRef` —
|
|
6798
|
+
// the choke point shared with the selector loops. Direct columns of the target arrive
|
|
6799
|
+
// under the `__collection_<table>__` marker or the bare table name; neither needs a join.
|
|
6800
|
+
if (!refAlias || refAlias === this.targetTable || refAlias.startsWith('__collection_') || alreadyJoined.has(refAlias)) {
|
|
6801
|
+
continue;
|
|
6802
|
+
}
|
|
6803
|
+
this.addNavigationJoinForFieldRef(ref, whereJoins, this.targetTable, targetSchema, whereAliases);
|
|
6804
|
+
}
|
|
6805
|
+
if (whereAliases.size > 0) {
|
|
6806
|
+
this.resolveNavigationJoins(whereAliases, whereJoins, targetSchema);
|
|
6807
|
+
}
|
|
6808
|
+
// A collection's correlation to its parent is implicit and always present, so an own
|
|
6809
|
+
// navigation named like the parent shadows it every time. Refuse it for the same reason
|
|
6810
|
+
// the standalone path does, or the identical logical query throws on one path and
|
|
6811
|
+
// misbinds on the other.
|
|
6812
|
+
(0, query_utils_2.assertNoCorrelatedAliasShadowing)(this.targetTable, [correlationAlias], new Set(whereJoins.map(nav => nav.alias)));
|
|
6813
|
+
return whereJoins.filter(nav => !alreadyJoined.has(nav.alias));
|
|
6814
|
+
}
|
|
6696
6815
|
addNavigationJoinForFieldRef(fieldRef, joins, sourceAlias, sourceSchema, allTableAliases, joinedAliases) {
|
|
6697
6816
|
if (!fieldRef || typeof fieldRef !== 'object' || !('__tableAlias' in fieldRef)) {
|
|
6698
6817
|
return;
|
|
6699
6818
|
}
|
|
6819
|
+
// A ref carrying a chain id was minted by the ENCLOSING query, not by this collection:
|
|
6820
|
+
// it is a correlation to the outer row, which the outer query already has in scope.
|
|
6821
|
+
// Refs this builder mints carry none — marker-aliased columns AND navigation traversals
|
|
6822
|
+
// alike — so this separates `s.library.name` (ours: join it) from `l.name` (outer: leave
|
|
6823
|
+
// it) even though both render under the alias `library`. Joining the latter pulls a
|
|
6824
|
+
// second copy of the outer table into the subquery and binds the correlation to it,
|
|
6825
|
+
// which silently makes the predicate compare the inner row with itself.
|
|
6826
|
+
//
|
|
6827
|
+
// Guarded HERE rather than at each caller because this method is the single choke point
|
|
6828
|
+
// through which the WHERE loop and all three selector loops resolve a ref into a join.
|
|
6829
|
+
// See isForeignChainRef.
|
|
6830
|
+
if ((0, query_utils_2.isForeignChainRef)(fieldRef, undefined)) {
|
|
6831
|
+
return;
|
|
6832
|
+
}
|
|
6700
6833
|
const tableAlias = fieldRef.__tableAlias;
|
|
6701
6834
|
// If this references the target table directly, no join needed
|
|
6702
6835
|
if (!tableAlias || tableAlias === this.targetTable) {
|
|
@@ -7279,6 +7412,14 @@ class CollectionQueryBuilder {
|
|
|
7279
7412
|
this.detectNavigationJoins(selectorResult, navigationJoins, this.targetTable, this.targetTableSchema);
|
|
7280
7413
|
}
|
|
7281
7414
|
}
|
|
7415
|
+
// The selector says nothing about the collection's own WHERE, so a navigation used only
|
|
7416
|
+
// there would render unjoined — and then bind to whatever the OUTER query has under that
|
|
7417
|
+
// alias instead of failing. Same resolution the inline EXISTS path uses.
|
|
7418
|
+
for (const nav of this.resolveWhereNavigationJoins(this.sourceTable)) {
|
|
7419
|
+
if (!navigationJoins.some(existing => existing.alias === nav.alias)) {
|
|
7420
|
+
navigationJoins.push(nav);
|
|
7421
|
+
}
|
|
7422
|
+
}
|
|
7282
7423
|
// Step 5b: Merge navigation path joins (for intermediate tables in navigation chains)
|
|
7283
7424
|
// These joins are needed when accessing a collection through a chain like:
|
|
7284
7425
|
// ln.edition.book.category.formats
|