linkgress-orm 0.4.80 → 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 +184 -91
- 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 +1 -1
|
@@ -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,30 +283,21 @@ 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
|
-
* chain — see isForeignChainRef.
|
|
280
|
-
*/
|
|
281
|
-
let chainIdSeq = 0;
|
|
282
|
-
/**
|
|
283
|
-
* Whether `ref` was minted by a query chain OTHER than `chainId` — i.e. it is a
|
|
284
|
-
* correlation to an enclosing query rather than something this builder owns.
|
|
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).
|
|
285
290
|
*
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
290
|
-
*
|
|
291
|
-
* row with itself — true for every row, no SQL error, no type error. Singular table names
|
|
292
|
-
* (`library` + `shelf.library`) produce that collision as a matter of course; the
|
|
293
|
-
* plural-table/singular-nav convention (`users` + `post.user`) hides it.
|
|
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.
|
|
294
296
|
*
|
|
295
|
-
*
|
|
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.
|
|
297
299
|
*/
|
|
298
|
-
|
|
300
|
+
let chainIdSeq = 0;
|
|
299
301
|
class QueryBuilder {
|
|
300
302
|
constructor(schema, client, whereCond, limit, offset, orderBy, executor, manualJoins, joinCounter, collectionStrategy, schemaRegistry) {
|
|
301
303
|
/** @internal Chain identity — see chainIdSeq. */
|
|
@@ -524,13 +526,13 @@ class QueryBuilder {
|
|
|
524
526
|
return cachedRow;
|
|
525
527
|
}
|
|
526
528
|
if (holder.prototype !== undefined && mock_row_cache_1.MockRowCache.isEnabled()) {
|
|
527
|
-
return (navCache[relName] = mintReferenceMockRow(holder.prototype));
|
|
529
|
+
return (navCache[relName] = mintReferenceMockRow(holder.prototype, slots[MOCK_ROW_CHAIN_ID]));
|
|
528
530
|
}
|
|
529
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
|
|
530
532
|
[], // Empty navigation path for first level navigation
|
|
531
533
|
sourceTableName // Pass source table name for lateral join correlation
|
|
532
534
|
);
|
|
533
|
-
return (navCache[relName] = refBuilder.createMockTargetRow(holder));
|
|
535
|
+
return (navCache[relName] = refBuilder.createMockTargetRow(holder, slots[MOCK_ROW_CHAIN_ID]));
|
|
534
536
|
},
|
|
535
537
|
enumerable: false,
|
|
536
538
|
configurable: true,
|
|
@@ -699,7 +701,7 @@ class QueryBuilder {
|
|
|
699
701
|
[], // Empty navigation path for first level navigation
|
|
700
702
|
schema.name // Pass source table name for lateral join correlation
|
|
701
703
|
);
|
|
702
|
-
return (navCache[relName] = refBuilder.createMockTargetRow());
|
|
704
|
+
return (navCache[relName] = refBuilder.createMockTargetRow(undefined, slots[MOCK_ROW_CHAIN_ID]));
|
|
703
705
|
},
|
|
704
706
|
enumerable: false,
|
|
705
707
|
configurable: true,
|
|
@@ -741,6 +743,11 @@ class SelectQueryBuilder {
|
|
|
741
743
|
return schema ? `"${schema}"."${tableName}"` : `"${tableName}"`;
|
|
742
744
|
}
|
|
743
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();
|
|
744
751
|
this.orderByFields = [];
|
|
745
752
|
this.manualJoins = [];
|
|
746
753
|
this.joinCounter = 0;
|
|
@@ -1017,7 +1024,7 @@ class SelectQueryBuilder {
|
|
|
1017
1024
|
* .select(g => ({ street: g.key.street, count: g.count() }))
|
|
1018
1025
|
*/
|
|
1019
1026
|
groupBy(selector) {
|
|
1020
|
-
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);
|
|
1021
1028
|
}
|
|
1022
1029
|
/**
|
|
1023
1030
|
* Add a LEFT JOIN with a subquery
|
|
@@ -1263,7 +1270,7 @@ class SelectQueryBuilder {
|
|
|
1263
1270
|
[], // Empty navigation path for first level navigation
|
|
1264
1271
|
schema.name // Pass source table name for lateral join correlation
|
|
1265
1272
|
);
|
|
1266
|
-
return (navCache[relName] = refBuilder.createMockTargetRow());
|
|
1273
|
+
return (navCache[relName] = refBuilder.createMockTargetRow(undefined, slots[MOCK_ROW_CHAIN_ID]));
|
|
1267
1274
|
},
|
|
1268
1275
|
enumerable: false,
|
|
1269
1276
|
configurable: true,
|
|
@@ -3728,7 +3735,7 @@ ${joinClauses.join('\n')}`;
|
|
|
3728
3735
|
return cachedRow;
|
|
3729
3736
|
}
|
|
3730
3737
|
if (holder.prototype !== undefined && mock_row_cache_1.MockRowCache.isEnabled()) {
|
|
3731
|
-
return (navCache[relName] = mintReferenceMockRow(holder.prototype));
|
|
3738
|
+
return (navCache[relName] = mintReferenceMockRow(holder.prototype, slots[MOCK_ROW_CHAIN_ID]));
|
|
3732
3739
|
}
|
|
3733
3740
|
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, targetSchema, // Pass the target schema directly
|
|
3734
3741
|
schemaRegistry, // Pass schema registry for nested resolution
|
|
@@ -3736,7 +3743,7 @@ ${joinClauses.join('\n')}`;
|
|
|
3736
3743
|
sourceTableName // Pass source table name for lateral join correlation
|
|
3737
3744
|
);
|
|
3738
3745
|
// Return a mock object that exposes the target table's columns
|
|
3739
|
-
return (navCache[relName] = refBuilder.createMockTargetRow(holder));
|
|
3746
|
+
return (navCache[relName] = refBuilder.createMockTargetRow(holder, slots[MOCK_ROW_CHAIN_ID]));
|
|
3740
3747
|
},
|
|
3741
3748
|
enumerable: false,
|
|
3742
3749
|
configurable: true,
|
|
@@ -3887,7 +3894,7 @@ ${joinClauses.join('\n')}`;
|
|
|
3887
3894
|
const columnName = nestedValue.__dbColumnName;
|
|
3888
3895
|
// Add JOIN if needed for navigation fields
|
|
3889
3896
|
if (tableAlias !== this.schema.name) {
|
|
3890
|
-
const relConfig = this.
|
|
3897
|
+
const relConfig = this.relationForRef(nestedValue, tableAlias);
|
|
3891
3898
|
if (relConfig && !joins.find(j => j.alias === tableAlias)) {
|
|
3892
3899
|
let targetSchema;
|
|
3893
3900
|
if (relConfig.targetTableBuilder) {
|
|
@@ -4031,7 +4038,7 @@ ${joinClauses.join('\n')}`;
|
|
|
4031
4038
|
const columnName = nestedValue.__dbColumnName;
|
|
4032
4039
|
// Add JOIN if needed for navigation fields
|
|
4033
4040
|
if (tableAlias !== this.schema.name) {
|
|
4034
|
-
const relConfig = this.
|
|
4041
|
+
const relConfig = this.relationForRef(nestedValue, tableAlias);
|
|
4035
4042
|
if (relConfig && !joins.find(j => j.alias === tableAlias)) {
|
|
4036
4043
|
let targetSchema;
|
|
4037
4044
|
if (relConfig.targetTableBuilder) {
|
|
@@ -4082,6 +4089,12 @@ ${joinClauses.join('\n')}`;
|
|
|
4082
4089
|
return;
|
|
4083
4090
|
}
|
|
4084
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
|
+
}
|
|
4085
4098
|
if (value && typeof value === 'object' && '__tableAlias' in value && '__dbColumnName' in value) {
|
|
4086
4099
|
// This is a FieldRef with a table alias
|
|
4087
4100
|
const tableAlias = value.__tableAlias;
|
|
@@ -4101,6 +4114,9 @@ ${joinClauses.join('\n')}`;
|
|
|
4101
4114
|
// SqlFragment may contain navigation property references
|
|
4102
4115
|
const fieldRefs = value.getFieldRefs();
|
|
4103
4116
|
for (const fieldRef of fieldRefs) {
|
|
4117
|
+
if ((0, query_utils_2.isForeignChainRef)(fieldRef, this.chainId)) {
|
|
4118
|
+
continue;
|
|
4119
|
+
}
|
|
4104
4120
|
if ('__tableAlias' in fieldRef && fieldRef.__tableAlias) {
|
|
4105
4121
|
const tableAlias = fieldRef.__tableAlias;
|
|
4106
4122
|
if (tableAlias && tableAlias !== this.schema.name) {
|
|
@@ -4123,6 +4139,25 @@ ${joinClauses.join('\n')}`;
|
|
|
4123
4139
|
}
|
|
4124
4140
|
}
|
|
4125
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
|
+
}
|
|
4126
4161
|
/**
|
|
4127
4162
|
* Resolve all navigation joins by finding the correct path through the schema graph
|
|
4128
4163
|
* This handles multi-level navigation like task.level.createdBy
|
|
@@ -4197,7 +4232,7 @@ ${joinClauses.join('\n')}`;
|
|
|
4197
4232
|
const tableAlias = fieldRef.__tableAlias;
|
|
4198
4233
|
if (tableAlias && tableAlias !== this.schema.name && !joins.some(j => j.alias === tableAlias)) {
|
|
4199
4234
|
// This references a related table - find the relation and add a JOIN
|
|
4200
|
-
const relation = this.
|
|
4235
|
+
const relation = this.relationForRef(fieldRef, tableAlias);
|
|
4201
4236
|
if (relation && relation.type === 'one') {
|
|
4202
4237
|
// Get target schema from targetTableBuilder if available
|
|
4203
4238
|
let targetSchema;
|
|
@@ -4233,7 +4268,7 @@ ${joinClauses.join('\n')}`;
|
|
|
4233
4268
|
// CORRELATION, not one of our navigations, and the outer query already has that
|
|
4234
4269
|
// table in scope. Joining it here would resolve the alias against OUR relations and
|
|
4235
4270
|
// pull in a second, inner copy of the outer table — see isForeignChainRef.
|
|
4236
|
-
if (isForeignChainRef(fieldRef, this.chainId)) {
|
|
4271
|
+
if ((0, query_utils_2.isForeignChainRef)(fieldRef, this.chainId)) {
|
|
4237
4272
|
if ('__tableAlias' in fieldRef && fieldRef.__tableAlias) {
|
|
4238
4273
|
correlatedAliases.add(fieldRef.__tableAlias);
|
|
4239
4274
|
}
|
|
@@ -4255,18 +4290,11 @@ ${joinClauses.join('\n')}`;
|
|
|
4255
4290
|
}
|
|
4256
4291
|
}
|
|
4257
4292
|
}
|
|
4258
|
-
//
|
|
4259
|
-
//
|
|
4260
|
-
|
|
4261
|
-
//
|
|
4262
|
-
|
|
4263
|
-
for (const alias of correlatedAliases) {
|
|
4264
|
-
if (allTableAliases.has(alias)) {
|
|
4265
|
-
throw new Error(`Correlated subquery over table "${this.schema.name}" both correlates to an outer "${alias}" and joins its own "${alias}" navigation. `
|
|
4266
|
-
+ `Both would use the alias "${alias}", so the inner join would shadow the outer table and the correlation would silently bind to the inner row. `
|
|
4267
|
-
+ `Traverse the navigation in the OUTER query, correlate on a plain key column instead of the navigation, or rename the navigation property.`);
|
|
4268
|
-
}
|
|
4269
|
-
}
|
|
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);
|
|
4270
4298
|
// Resolve all joins through the schema graph
|
|
4271
4299
|
this.resolveJoinsForTableAliases(allTableAliases, joins);
|
|
4272
4300
|
}
|
|
@@ -4287,6 +4315,10 @@ ${joinClauses.join('\n')}`;
|
|
|
4287
4315
|
this.detectAndAddJoinsFromSelection(selection, joins);
|
|
4288
4316
|
// Scan WHERE condition for navigation property references and add JOINs
|
|
4289
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)));
|
|
4290
4322
|
// Handle case where selection is a single value (not an object with properties)
|
|
4291
4323
|
if (selection instanceof conditions_1.SqlFragment) {
|
|
4292
4324
|
// Single SQL fragment - just build it directly
|
|
@@ -4357,7 +4389,7 @@ ${joinClauses.join('\n')}`;
|
|
|
4357
4389
|
const tableAlias = value.__tableAlias;
|
|
4358
4390
|
const columnName = value.__dbColumnName;
|
|
4359
4391
|
// Find the relation config for this navigation
|
|
4360
|
-
const relConfig = this.
|
|
4392
|
+
const relConfig = this.relationForRef(value, tableAlias);
|
|
4361
4393
|
if (relConfig) {
|
|
4362
4394
|
// Add JOIN if not already added
|
|
4363
4395
|
if (!joins.find(j => j.alias === tableAlias)) {
|
|
@@ -4459,7 +4491,7 @@ ${joinClauses.join('\n')}`;
|
|
|
4459
4491
|
const firstValue = value[tableAlias];
|
|
4460
4492
|
if (firstValue && typeof firstValue === 'object' && '__tableAlias' in firstValue) {
|
|
4461
4493
|
const alias = firstValue.__tableAlias;
|
|
4462
|
-
const relConfig = this.
|
|
4494
|
+
const relConfig = this.relationForRef(firstValue, alias);
|
|
4463
4495
|
if (relConfig && relConfig.type === 'one') {
|
|
4464
4496
|
// This is a reference navigation - select all fields from the target table
|
|
4465
4497
|
// Performance: Use cached target schema
|
|
@@ -4757,6 +4789,10 @@ ${joinClauses.join('\n')}`;
|
|
|
4757
4789
|
this.detectAndAddJoinsFromSelection(selection, joins);
|
|
4758
4790
|
// Scan WHERE condition for navigation property references and add JOINs
|
|
4759
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)));
|
|
4760
4796
|
// Handle case where selection is a single value (not an object with properties)
|
|
4761
4797
|
if (selection instanceof conditions_1.SqlFragment) {
|
|
4762
4798
|
const sqlBuildContext = {
|
|
@@ -4831,7 +4867,7 @@ ${joinClauses.join('\n')}`;
|
|
|
4831
4867
|
if ('__tableAlias' in value && value.__tableAlias && typeof value.__tableAlias === 'string') {
|
|
4832
4868
|
const tableAlias = value.__tableAlias;
|
|
4833
4869
|
const columnName = value.__dbColumnName;
|
|
4834
|
-
const relConfig = this.
|
|
4870
|
+
const relConfig = this.relationForRef(value, tableAlias);
|
|
4835
4871
|
if (relConfig && !joins.find(j => j.alias === tableAlias)) {
|
|
4836
4872
|
let targetSchema;
|
|
4837
4873
|
if (relConfig.targetTableBuilder) {
|
|
@@ -5861,7 +5897,7 @@ ${joinClauses.join('\n')}`;
|
|
|
5861
5897
|
// correlation whatever it is called, and reading it as our own navigation (because a
|
|
5862
5898
|
// relation happens to carry the same name) is what silently misbinds the predicate.
|
|
5863
5899
|
// See isForeignChainRef.
|
|
5864
|
-
if (tableAlias !== currentTableName && isForeignChainRef(ref, this.chainId)) {
|
|
5900
|
+
if (tableAlias !== currentTableName && (0, query_utils_2.isForeignChainRef)(ref, this.chainId)) {
|
|
5865
5901
|
outerRefs.push(ref);
|
|
5866
5902
|
}
|
|
5867
5903
|
else if (tableAlias !== currentTableName && !this.schema.relations[tableAlias]) {
|
|
@@ -5949,7 +5985,7 @@ class ReferenceQueryBuilder {
|
|
|
5949
5985
|
* Create a mock object that exposes the target table's columns
|
|
5950
5986
|
* This allows accessing related fields like: p.user.username
|
|
5951
5987
|
*/
|
|
5952
|
-
createMockTargetRow(holder) {
|
|
5988
|
+
createMockTargetRow(holder, chainId) {
|
|
5953
5989
|
if (this.targetTableSchema) {
|
|
5954
5990
|
// Prototype-level cache — see MockRowCache's doc. Everything the getters close over
|
|
5955
5991
|
// is fully determined by (target schema object identity, relationName, sourceAlias,
|
|
@@ -5976,7 +6012,7 @@ class ReferenceQueryBuilder {
|
|
|
5976
6012
|
holder.prototype = prototype;
|
|
5977
6013
|
}
|
|
5978
6014
|
}
|
|
5979
|
-
return mintReferenceMockRow(prototype);
|
|
6015
|
+
return mintReferenceMockRow(prototype, chainId);
|
|
5980
6016
|
}
|
|
5981
6017
|
else {
|
|
5982
6018
|
// Fallback: use the shared nested proxy that supports deep property access
|
|
@@ -6020,6 +6056,8 @@ class ReferenceQueryBuilder {
|
|
|
6020
6056
|
__fieldName: colName,
|
|
6021
6057
|
__dbColumnName: dbColumnName,
|
|
6022
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],
|
|
6023
6061
|
__sourceTable: sourceTable, // Actual table name for mapper lookup
|
|
6024
6062
|
__mapper: mapper, // Include mapper for toDriver transformation in conditions
|
|
6025
6063
|
__sqlType: columnSqlTypes[colName], // Column SQL type — lets flag* emit width-exact mask casts
|
|
@@ -6103,7 +6141,7 @@ class ReferenceQueryBuilder {
|
|
|
6103
6141
|
let cached = navCache[relName];
|
|
6104
6142
|
if (cached === undefined) {
|
|
6105
6143
|
if (holder.prototype !== undefined && mock_row_cache_1.MockRowCache.isEnabled()) {
|
|
6106
|
-
cached = navCache[relName] = mintReferenceMockRow(holder.prototype);
|
|
6144
|
+
cached = navCache[relName] = mintReferenceMockRow(holder.prototype, slots[MOCK_ROW_CHAIN_ID]);
|
|
6107
6145
|
}
|
|
6108
6146
|
else {
|
|
6109
6147
|
const refBuilder = new ReferenceQueryBuilder(relName, relConfig.targetTable, relConfig.foreignKeys || [relConfig.foreignKey || ''], relConfig.matches || [], relConfig.isMandatory ?? false, nestedTargetSchema, // Pass the target schema directly
|
|
@@ -6111,7 +6149,7 @@ class ReferenceQueryBuilder {
|
|
|
6111
6149
|
extendedNavPath, // Pass navigation path for nested collections
|
|
6112
6150
|
parentSourceAlias ? tableAlias : '' // Only set source if tracking path
|
|
6113
6151
|
);
|
|
6114
|
-
cached = navCache[relName] = refBuilder.createMockTargetRow(holder);
|
|
6152
|
+
cached = navCache[relName] = refBuilder.createMockTargetRow(holder, slots[MOCK_ROW_CHAIN_ID]);
|
|
6115
6153
|
}
|
|
6116
6154
|
}
|
|
6117
6155
|
return cached;
|
|
@@ -6306,14 +6344,14 @@ class CollectionQueryBuilder {
|
|
|
6306
6344
|
return cachedRow;
|
|
6307
6345
|
}
|
|
6308
6346
|
if (holder.prototype !== undefined && mock_row_cache_1.MockRowCache.isEnabled()) {
|
|
6309
|
-
return (navCache[relName] = mintReferenceMockRow(holder.prototype));
|
|
6347
|
+
return (navCache[relName] = mintReferenceMockRow(holder.prototype, slots[MOCK_ROW_CHAIN_ID]));
|
|
6310
6348
|
}
|
|
6311
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
|
|
6312
6350
|
schemaRegistry, // Pass schema registry for nested resolution
|
|
6313
6351
|
[], // Empty navigation path - this is the first reference in the chain
|
|
6314
6352
|
targetTable // Source alias is this collection's target table
|
|
6315
6353
|
);
|
|
6316
|
-
return (navCache[relName] = refBuilder.createMockTargetRow(holder));
|
|
6354
|
+
return (navCache[relName] = refBuilder.createMockTargetRow(holder, slots[MOCK_ROW_CHAIN_ID]));
|
|
6317
6355
|
},
|
|
6318
6356
|
enumerable: false,
|
|
6319
6357
|
configurable: true,
|
|
@@ -6545,11 +6583,22 @@ class CollectionQueryBuilder {
|
|
|
6545
6583
|
}
|
|
6546
6584
|
/**
|
|
6547
6585
|
* Get field references from this condition.
|
|
6548
|
-
*
|
|
6549
|
-
*
|
|
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.
|
|
6550
6596
|
*/
|
|
6551
6597
|
getFieldRefs() {
|
|
6552
|
-
|
|
6598
|
+
if (!this.whereCond) {
|
|
6599
|
+
return [];
|
|
6600
|
+
}
|
|
6601
|
+
return this.whereCond.getFieldRefs().filter(ref => (0, query_utils_2.isForeignChainRef)(ref, undefined));
|
|
6553
6602
|
}
|
|
6554
6603
|
/**
|
|
6555
6604
|
* Build SQL for this collection as a correlated EXISTS subquery.
|
|
@@ -6596,38 +6645,11 @@ class CollectionQueryBuilder {
|
|
|
6596
6645
|
// the statement failed with `missing FROM-clause entry for table "<alias>"`.
|
|
6597
6646
|
// Reuses the selector path's machinery: seed aliases + chains from the
|
|
6598
6647
|
// condition's FieldRefs, then resolve multi-hop paths against the target schema.
|
|
6599
|
-
|
|
6600
|
-
const
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6604
|
-
const targetSchema = this.schemaRegistry?.get(targetTable);
|
|
6605
|
-
if (targetSchema) {
|
|
6606
|
-
const whereJoins = [];
|
|
6607
|
-
const whereAliases = new Set();
|
|
6608
|
-
for (const ref of this.whereCond.getFieldRefs()) {
|
|
6609
|
-
const refAlias = ref?.__tableAlias;
|
|
6610
|
-
// Direct columns of the target arrive under the `__collection_<table>__`
|
|
6611
|
-
// marker (rewritten below) or the bare table name — neither needs a join.
|
|
6612
|
-
if (!refAlias || refAlias === targetTable || refAlias.startsWith('__collection_') || joinedAliases.has(refAlias)) {
|
|
6613
|
-
continue;
|
|
6614
|
-
}
|
|
6615
|
-
this.addNavigationJoinForFieldRef(ref, whereJoins, targetTable, targetSchema, whereAliases);
|
|
6616
|
-
}
|
|
6617
|
-
if (whereAliases.size > 0) {
|
|
6618
|
-
this.resolveNavigationJoins(whereAliases, whereJoins, targetSchema);
|
|
6619
|
-
}
|
|
6620
|
-
for (const nav of whereJoins) {
|
|
6621
|
-
if (joinedAliases.has(nav.alias)) {
|
|
6622
|
-
continue;
|
|
6623
|
-
}
|
|
6624
|
-
joinedAliases.add(nav.alias);
|
|
6625
|
-
const joinType = nav.isMandatory ? 'JOIN' : 'LEFT JOIN';
|
|
6626
|
-
const fk = nav.foreignKeys[0];
|
|
6627
|
-
const pk = (nav.matches && nav.matches.length > 0) ? nav.matches[0] : 'id';
|
|
6628
|
-
allJoins.push(`${joinType} "${nav.targetTable}" "${nav.alias}" ON "${nav.sourceAlias}"."${fk}" = "${nav.alias}"."${pk}"`);
|
|
6629
|
-
}
|
|
6630
|
-
}
|
|
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}"`);
|
|
6631
6653
|
}
|
|
6632
6654
|
const navJoinsSQL = allJoins.join('\n');
|
|
6633
6655
|
// Build WHERE clause: correlation + additional conditions
|
|
@@ -6741,10 +6763,73 @@ class CollectionQueryBuilder {
|
|
|
6741
6763
|
* Add a navigation JOIN for a FieldRef if it references a related table
|
|
6742
6764
|
* Handles multi-level navigation by recursively resolving the join chain
|
|
6743
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
|
+
}
|
|
6744
6815
|
addNavigationJoinForFieldRef(fieldRef, joins, sourceAlias, sourceSchema, allTableAliases, joinedAliases) {
|
|
6745
6816
|
if (!fieldRef || typeof fieldRef !== 'object' || !('__tableAlias' in fieldRef)) {
|
|
6746
6817
|
return;
|
|
6747
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
|
+
}
|
|
6748
6833
|
const tableAlias = fieldRef.__tableAlias;
|
|
6749
6834
|
// If this references the target table directly, no join needed
|
|
6750
6835
|
if (!tableAlias || tableAlias === this.targetTable) {
|
|
@@ -7327,6 +7412,14 @@ class CollectionQueryBuilder {
|
|
|
7327
7412
|
this.detectNavigationJoins(selectorResult, navigationJoins, this.targetTable, this.targetTableSchema);
|
|
7328
7413
|
}
|
|
7329
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
|
+
}
|
|
7330
7423
|
// Step 5b: Merge navigation path joins (for intermediate tables in navigation chains)
|
|
7331
7424
|
// These joins are needed when accessing a collection through a chain like:
|
|
7332
7425
|
// ln.edition.book.category.formats
|