linkgress-orm 0.4.80 → 0.4.83

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.
Files changed (34) hide show
  1. package/dist/entity/entity-base.d.ts +5 -0
  2. package/dist/entity/entity-base.d.ts.map +1 -1
  3. package/dist/entity/entity-base.js.map +1 -1
  4. package/dist/entity/entity-builder.d.ts +18 -0
  5. package/dist/entity/entity-builder.d.ts.map +1 -1
  6. package/dist/entity/entity-builder.js +42 -0
  7. package/dist/entity/entity-builder.js.map +1 -1
  8. package/dist/migration/db-schema-manager.d.ts +4 -0
  9. package/dist/migration/db-schema-manager.d.ts.map +1 -1
  10. package/dist/migration/db-schema-manager.js +14 -1
  11. package/dist/migration/db-schema-manager.js.map +1 -1
  12. package/dist/migration/index-sql.d.ts +20 -5
  13. package/dist/migration/index-sql.d.ts.map +1 -1
  14. package/dist/migration/index-sql.js +55 -24
  15. package/dist/migration/index-sql.js.map +1 -1
  16. package/dist/migration/migration-scaffold.d.ts.map +1 -1
  17. package/dist/migration/migration-scaffold.js +1 -0
  18. package/dist/migration/migration-scaffold.js.map +1 -1
  19. package/dist/query/grouped-query.d.ts +6 -2
  20. package/dist/query/grouped-query.d.ts.map +1 -1
  21. package/dist/query/grouped-query.js +28 -3
  22. package/dist/query/grouped-query.js.map +1 -1
  23. package/dist/query/query-builder.d.ts +46 -3
  24. package/dist/query/query-builder.d.ts.map +1 -1
  25. package/dist/query/query-builder.js +200 -91
  26. package/dist/query/query-builder.js.map +1 -1
  27. package/dist/query/query-utils.d.ts +41 -0
  28. package/dist/query/query-utils.d.ts.map +1 -1
  29. package/dist/query/query-utils.js +56 -0
  30. package/dist/query/query-utils.js.map +1 -1
  31. package/dist/schema/table-builder.d.ts +5 -0
  32. package/dist/schema/table-builder.d.ts.map +1 -1
  33. package/dist/schema/table-builder.js.map +1 -1
  34. 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
- /** A reference mock row: `Object.create(prototype)` plus its two own state slots. */
125
- const mintReferenceMockRow = (prototype) => {
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
- * fresh id; derived builders inherit it (navigation sub-builders are handed the
277
- * parent's id, so a nav ref shares its root's chain). Field refs bake the id so a
278
- * standalone subquery can tell its OWN refs from ones leaking in from an OUTER
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
- * Alias identity alone cannot answer this. A subquery's own navigation aliases are its
287
- * relation NAMES, and a relation name may coincide with an outer table's alias: a child
288
- * with a `library` navigation, correlated against a parent table also called `library`.
289
- * Resolving such a ref by name makes the builder join a second, inner copy of the parent
290
- * and bind the correlation to it, which turns the predicate into a comparison of the inner
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
- * Returns false when either side lacks an id, so refs from paths that do not stamp one keep
296
- * their previous, name-based treatment.
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
- const isForeignChainRef = (ref, chainId) => ref?.__chainId != null && chainId != null && ref.__chainId !== chainId;
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.schema.relations[tableAlias];
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.schema.relations[tableAlias];
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.schema.relations[tableAlias];
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
- // A correlation whose alias is ALSO one of our own navigation aliases cannot be
4259
- // rendered: both would occupy the same identifier in one scope, our join would shadow
4260
- // the outer table, and the correlation predicate would bind to the inner row — the
4261
- // silent wrong answer this whole path exists to prevent. Same contract as the
4262
- // same-table guard in extractOuterFieldRefs: refuse loudly rather than misbind.
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.schema.relations[tableAlias];
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.schema.relations[alias];
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.schema.relations[tableAlias];
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
- * Returns empty since EXISTS subqueries are self-contained correlated subqueries.
6549
- * Required for duck-typing compatibility with Condition interface when used in WHERE clauses.
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
- return [];
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
- if (this.whereCond) {
6600
- const joinedAliases = new Set([
6601
- ...this.navigationPath.map(nav => nav.alias),
6602
- ...this.selectManyJoins.map(nav => nav.alias),
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,89 @@ 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 navigation of ours named like the collection's PARENT needs care: the collection's
6809
+ // correlation to that parent is implicit and always present, so both want one alias.
6810
+ //
6811
+ // Whether that is a problem depends on whether the navigation is the INVERSE of the
6812
+ // collection's own foreign key:
6813
+ //
6814
+ // - Same key pair (`userEshop.cards` correlating on `card.user_id = user_eshop.id`, and
6815
+ // the card's own `userEshop` navigation joining on exactly that) — the navigation IS
6816
+ // the parent row. The join would be the identity, so it is dropped and the alias keeps
6817
+ // denoting the parent, which is what the reference meant. Rendering the join instead
6818
+ // would shadow the correlation and detach every child from its parent.
6819
+ // - A DIFFERENT key pair — the navigation points at another row of the parent's table, so
6820
+ // the alias would have to mean two things at once. That cannot be rendered; refuse it.
6821
+ const sameKeys = (a, b) => {
6822
+ const left = a ?? [];
6823
+ const right = b ?? [];
6824
+ return left.length === right.length && left.every((key, index) => key === right[index]);
6825
+ };
6826
+ const inverseOfCollectionKey = (nav) => sameKeys(nav.foreignKeys, this.foreignKeys) && sameKeys(nav.matches, this.matches);
6827
+ const clashing = whereJoins.filter(nav => nav.alias === correlationAlias);
6828
+ (0, query_utils_2.assertNoCorrelatedAliasShadowing)(this.targetTable, [correlationAlias], new Set(clashing.filter(nav => !inverseOfCollectionKey(nav)).map(nav => nav.alias)));
6829
+ return whereJoins.filter(nav => !alreadyJoined.has(nav.alias) && nav.alias !== correlationAlias);
6830
+ }
6744
6831
  addNavigationJoinForFieldRef(fieldRef, joins, sourceAlias, sourceSchema, allTableAliases, joinedAliases) {
6745
6832
  if (!fieldRef || typeof fieldRef !== 'object' || !('__tableAlias' in fieldRef)) {
6746
6833
  return;
6747
6834
  }
6835
+ // A ref carrying a chain id was minted by the ENCLOSING query, not by this collection:
6836
+ // it is a correlation to the outer row, which the outer query already has in scope.
6837
+ // Refs this builder mints carry none — marker-aliased columns AND navigation traversals
6838
+ // alike — so this separates `s.library.name` (ours: join it) from `l.name` (outer: leave
6839
+ // it) even though both render under the alias `library`. Joining the latter pulls a
6840
+ // second copy of the outer table into the subquery and binds the correlation to it,
6841
+ // which silently makes the predicate compare the inner row with itself.
6842
+ //
6843
+ // Guarded HERE rather than at each caller because this method is the single choke point
6844
+ // through which the WHERE loop and all three selector loops resolve a ref into a join.
6845
+ // See isForeignChainRef.
6846
+ if ((0, query_utils_2.isForeignChainRef)(fieldRef, undefined)) {
6847
+ return;
6848
+ }
6748
6849
  const tableAlias = fieldRef.__tableAlias;
6749
6850
  // If this references the target table directly, no join needed
6750
6851
  if (!tableAlias || tableAlias === this.targetTable) {
@@ -7327,6 +7428,14 @@ class CollectionQueryBuilder {
7327
7428
  this.detectNavigationJoins(selectorResult, navigationJoins, this.targetTable, this.targetTableSchema);
7328
7429
  }
7329
7430
  }
7431
+ // The selector says nothing about the collection's own WHERE, so a navigation used only
7432
+ // there would render unjoined — and then bind to whatever the OUTER query has under that
7433
+ // alias instead of failing. Same resolution the inline EXISTS path uses.
7434
+ for (const nav of this.resolveWhereNavigationJoins(this.sourceTable)) {
7435
+ if (!navigationJoins.some(existing => existing.alias === nav.alias)) {
7436
+ navigationJoins.push(nav);
7437
+ }
7438
+ }
7330
7439
  // Step 5b: Merge navigation path joins (for intermediate tables in navigation chains)
7331
7440
  // These joins are needed when accessing a collection through a chain like:
7332
7441
  // ln.edition.book.category.formats