fix: add missing LEFT JOIN for relation fields in groupBy orderByForRecords (#18798)

## Summary

Fixes "missing FROM-clause entry for table" SQL error when using
`orderByForRecords` with a relation field (e.g. `{ company: { name:
"AscNullsFirst" } }`) in a `groupBy` query.

### Root cause

This is a regression from #18005 (Feb 17). That PR correctly removed a
duplicate `applyOrderToBuilder()` call on the groupBy subquery (which
was conflicting with the `ROW_NUMBER() OVER (... ORDER BY ...)` window
function), but in doing so it also removed the LEFT JOINs that
`applyOrderToBuilder` was adding for relation fields.

After that change, ordering relied solely on `getOrderByRawSQL()` inside
`applyPartitionByToBuilder()`, which builds raw SQL for the window
function but never added the required JOINs. Scalar field ordering (e.g.
`name`, `position`) kept working since those don't need JOINs, but
relation field ordering (e.g. `company.name`) broke.

### Fix

- `getOrderByRawSQL` now returns `relationJoins` alongside the SQL
string so callers get the join info they need
- `applyPartitionByToBuilder` in `GroupByWithRecordsService` adds the
required LEFT JOINs before building the `ROW_NUMBER()` window function,
mirroring the pattern used by `applyOrderToBuilder` in the `findMany`
path

## Test plan

- [x] Manually tested locally with a GraphQL query matching the
customer's failing pattern (`orderByForRecords: [{ company: { name:
"AscNullsFirst" } }]`)
- [x] Added integration tests for ascending and descending relation
field ordering in `group-by-with-records-resolver.integration-spec.ts`
- [x] Typecheck passes
- [x] Lint passes
- [x] CI green
This commit is contained in:
Charles Bochet
2026-03-20 15:02:35 +01:00
committed by GitHub
parent 698c15b8cd
commit 217adc8d17
3 changed files with 157 additions and 7 deletions
@@ -13,6 +13,7 @@ import { GraphqlQueryOrderGroupByParser } from 'src/engine/api/graphql/graphql-q
import {
GraphqlQueryOrderFieldParser,
type OrderByClause,
type RelationJoinInfo,
} from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser';
import {
GraphqlQuerySelectedFieldsParser,
@@ -172,7 +173,7 @@ export class GraphqlQueryParser {
orderBy: ObjectRecordOrderBy | OrderByWithGroupBy,
objectNameSingular: string,
isForwardPagination = true,
): string {
): { orderByRawSQL: string; relationJoins: RelationJoinInfo[] } {
const parseResult = this.orderFieldParser.parse(
orderBy as ObjectRecordOrderBy,
objectNameSingular,
@@ -208,11 +209,11 @@ export class GraphqlQueryParser {
const orderByRawSQLString = orderByRawSQLClauseArray.join(', ');
const orderByCompleteSQLClause = isNonEmptyString(orderByRawSQLString)
const orderByRawSQL = isNonEmptyString(orderByRawSQLString)
? `ORDER BY ${orderByRawSQLString}`
: '';
return orderByCompleteSQLClause;
return { orderByRawSQL, relationJoins: parseResult.relationJoins };
}
public applyGroupByOrderToBuilder(
@@ -264,12 +264,28 @@ export class GroupByWithRecordsService {
flatFieldMetadataMaps,
);
const orderByRawSQL = graphqlQueryParser.getOrderByRawSQL(
orderByForRecords,
flatObjectMetadata.nameSingular,
);
const { orderByRawSQL, relationJoins } =
graphqlQueryParser.getOrderByRawSQL(
orderByForRecords,
flatObjectMetadata.nameSingular,
);
if (isNonEmptyString(orderByRawSQL)) {
const existingJoinAliases = new Set(
queryBuilder.expressionMap.joinAttributes.map(
(joinAttribute) => joinAttribute.alias.name,
),
);
for (const joinInfo of relationJoins) {
if (!existingJoinAliases.has(joinInfo.joinAlias)) {
queryBuilder.leftJoin(
`${flatObjectMetadata.nameSingular}.${joinInfo.joinAlias}`,
joinInfo.joinAlias,
);
}
}
return queryBuilder.addSelect(
`ROW_NUMBER() OVER (PARTITION BY ${groupByExpressions} ${orderByRawSQL})`,
'record_row_number',