linkgress-orm 0.4.54 → 0.4.56

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.
@@ -3021,11 +3021,11 @@ class SelectQueryBuilder {
3021
3021
  joinClauses.push(collection.joinClause);
3022
3022
  }
3023
3023
  // Build the final CTE query
3024
- const sql = `WITH "__mutation__" AS (
3025
- ${mutationWithReturning}
3026
- )
3027
- SELECT ${selectParts.join(', ')}
3028
- FROM "__mutation__"
3024
+ const sql = `WITH "__mutation__" AS (
3025
+ ${mutationWithReturning}
3026
+ )
3027
+ SELECT ${selectParts.join(', ')}
3028
+ FROM "__mutation__"
3029
3029
  ${joinClauses.join('\n')}`;
3030
3030
  return { sql, params: allParams, nestedPaths };
3031
3031
  }
@@ -6019,6 +6019,23 @@ class CollectionQueryBuilder {
6019
6019
  if (!tableAlias || tableAlias === this.targetTable) {
6020
6020
  return;
6021
6021
  }
6022
+ // Seed the chain the projection ACTUALLY navigated, not just its terminal alias.
6023
+ // Without this, a projection that names only the deep leaf (e.g. `oi.productPrice.product.resort.name`
6024
+ // with no sibling scalar off `productPrice` / `product`) leaves the intermediates out of
6025
+ // `allTableAliases` entirely, so resolveNavigationJoins' direct-relation phase has nothing to
6026
+ // anchor on and falls through to the name-based schema-graph BFS - which happily reaches
6027
+ // `resort` one hop sooner through an unrelated same-table FK (`order_item.cashback_product_id`)
6028
+ // and silently returns another row's data.
6029
+ // This is the same `__navigationAliases` signal QueryBuilder.collectTableAliasesFromSelection,
6030
+ // GroupedQueryBuilder and DbContext already consume, and the same treatment nested
6031
+ // CollectionQueryBuilder values already get in detectNavigationJoins.
6032
+ if (Array.isArray(fieldRef.__navigationAliases)) {
6033
+ for (const navAlias of fieldRef.__navigationAliases) {
6034
+ if (navAlias && navAlias !== this.targetTable) {
6035
+ allTableAliases.add(navAlias);
6036
+ }
6037
+ }
6038
+ }
6022
6039
  // Collect this table alias for later resolution
6023
6040
  allTableAliases.add(tableAlias);
6024
6041
  // Check if we already have this join
@@ -6087,39 +6104,69 @@ class CollectionQueryBuilder {
6087
6104
  joinedSchemas.set(join.alias, schema);
6088
6105
  }
6089
6106
  }
6090
- // Try to resolve each unresolved alias
6107
+ // PHASE 1 - direct relation lookups, run to a FIXPOINT.
6108
+ // A join added here immediately becomes an anchor candidate for the remaining aliases in
6109
+ // this very pass, so a parent that is itself several hops away from the root (e.g.
6110
+ // order_item -> product_price -> product) can still anchor its own children. Without the
6111
+ // fixpoint those children never see their real parent (joinedSchemas was snapshotted before
6112
+ // the parent was joined) and fall through to the BFS below, which anchors them on whatever
6113
+ // other relation happens to point at the same table - e.g.
6114
+ // order_item.cashback_product_id -> product - silently returning another row's data.
6115
+ let progressed = true;
6116
+ // Termination (why no maxIterations guard is needed here, unlike every other loop in this
6117
+ // file): `progressed` is only ever set on a path that also adds a NEW alias to `resolved`
6118
+ // - every other path `continue`s - so `resolved` grows strictly monotonically and is
6119
+ // bounded above by `allTableAliases.size`. The loop therefore runs at most
6120
+ // `allTableAliases.size + 1` times.
6121
+ while (progressed) {
6122
+ progressed = false;
6123
+ for (const alias of allTableAliases) {
6124
+ if (resolved.has(alias)) {
6125
+ continue;
6126
+ }
6127
+ if (joins.some(j => j.alias === alias)) {
6128
+ resolved.add(alias);
6129
+ continue;
6130
+ }
6131
+ // Look for this alias in any of the already joined schemas (direct lookup)
6132
+ for (const [schemaAlias, schema] of joinedSchemas) {
6133
+ const relation = schema.relations?.[alias];
6134
+ if (!relation || relation.type !== 'one') {
6135
+ continue;
6136
+ }
6137
+ const targetSchema = this.addNavigationJoin(alias, relation, joins, schemaAlias);
6138
+ if (targetSchema) {
6139
+ // Keep the anchor map current so the alias we just joined can serve as the
6140
+ // source for further direct lookups in the next fixpoint round
6141
+ joinedSchemas.set(alias, targetSchema);
6142
+ }
6143
+ resolved.add(alias);
6144
+ progressed = true;
6145
+ break;
6146
+ }
6147
+ }
6148
+ }
6149
+ // PHASE 2 - only aliases with no direct relation path at all fall back to the transitive
6150
+ // schema-graph BFS, which searches by relation NAME and therefore cannot tell which of
6151
+ // several relations pointing at the same table the projection actually meant
6091
6152
  for (const alias of allTableAliases) {
6092
- if (resolved.has(alias) || joins.some(j => j.alias === alias)) {
6093
- resolved.add(alias);
6153
+ if (resolved.has(alias) || !this.schemaRegistry) {
6094
6154
  continue;
6095
6155
  }
6096
- // First, look for this alias in any of the already joined schemas (direct lookup)
6097
- let found = false;
6098
- for (const [schemaAlias, schema] of joinedSchemas) {
6099
- if (schema.relations && schema.relations[alias]) {
6100
- const relation = schema.relations[alias];
6101
- if (relation.type === 'one') {
6102
- this.addNavigationJoin(alias, relation, joins, schemaAlias);
6103
- resolved.add(alias);
6104
- found = true;
6105
- break;
6106
- }
6107
- }
6156
+ const path = this.findNavigationPath(alias, joinedSchemas, startSchema);
6157
+ if (path.length === 0) {
6158
+ continue;
6108
6159
  }
6109
- // If not found directly, search transitively through all schemas in registry
6110
- // to find an intermediate path
6111
- if (!found && this.schemaRegistry) {
6112
- const path = this.findNavigationPath(alias, joinedSchemas, startSchema);
6113
- if (path.length > 0) {
6114
- // Add all intermediate joins
6115
- for (const step of path) {
6116
- if (!joins.some(j => j.alias === step.alias)) {
6117
- this.addNavigationJoin(step.alias, step.relation, joins, step.sourceAlias);
6118
- }
6160
+ // Add all intermediate joins
6161
+ for (const step of path) {
6162
+ if (!joins.some(j => j.alias === step.alias)) {
6163
+ const stepSchema = this.addNavigationJoin(step.alias, step.relation, joins, step.sourceAlias);
6164
+ if (stepSchema) {
6165
+ joinedSchemas.set(step.alias, stepSchema);
6119
6166
  }
6120
- resolved.add(alias);
6121
6167
  }
6122
6168
  }
6169
+ resolved.add(alias);
6123
6170
  }
6124
6171
  }
6125
6172
  }