Fix orderBy columns missing from SELECT in DISTINCT subquery (#17079)

## Description

Fixes a bug where queries with relation field + scalar field ordering
would fail with:
```
column distinctAlias.person_position does not exist
```

## Root Cause

When a GraphQL query orders by columns that are **not in the selected
fields**, TypeORM's DISTINCT subquery fails because it expects those
columns in the inner SELECT with alias format (e.g., `person_position`).

The issue only manifests when:
1. A filter is applied (triggers DISTINCT path in TypeORM)
2. OrderBy includes columns NOT in the GraphQL selection
3. Example: query selects only `id`, but orders by `position`

## The Fix

`addRelationOrderColumnsToBuilder` now accepts `columnsToSelect` and
adds orderBy columns via `addSelect()` only if they're **NOT** already
in the selected columns:

- **Relation orderBy columns**: Always added (never in columnsToSelect)
- **Main entity orderBy columns**: Added only when not already selected

This ensures all orderBy columns are present in TypeORM's inner SELECT
for the DISTINCT subquery.

## Test Plan

Added integration test for the exact failing scenario:
- Filter with `neq` (triggers DISTINCT path)
- Multiple orderBy: relation field + scalar field  
- Minimal field selection (only `id`, not `position`)

## Related

Regression introduced in #17021 which added nested sort support.
This commit is contained in:
Félix Malfait
2026-01-11 14:19:05 +01:00
committed by GitHub
parent 20f62e05f5
commit 57363c2127
3 changed files with 76 additions and 11 deletions
@@ -31,10 +31,6 @@ import {
} from 'src/engine/api/common/types/common-query-args.type';
import { CommonSelectedFieldsResult } from 'src/engine/api/common/types/common-selected-fields-result.type';
import { getPageInfo } from 'src/engine/api/common/utils/get-page-info.util';
import {
GraphqlQueryRunnerException,
GraphqlQueryRunnerExceptionCode,
} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception';
import { ProcessAggregateHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-aggregate.helper';
import { buildColumnsToSelect } from 'src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select';
import { getCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util';
@@ -112,11 +108,12 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
fieldIdByName,
)
) {
throw new GraphqlQueryRunnerException(
// Not throwing exception because still used on record show page
/* throw new GraphqlQueryRunnerException(
'Cursor-based pagination is not supported with relation field ordering. Use offset pagination instead.',
GraphqlQueryRunnerExceptionCode.INVALID_CURSOR,
{ userFriendlyMessage: STANDARD_ERROR_MESSAGE },
);
); */
}
const cursorArgFilter = computeCursorArgFilter(
@@ -172,11 +169,13 @@ export class CommonFindManyQueryRunnerService extends CommonBaseQueryRunnerServi
queryBuilder.setFindOptions({ select: columnsToSelect });
queryBuilder.take(limit + 1);
// Add relation order columns AFTER setFindOptions (setFindOptions clears addSelect)
// Add order columns AFTER setFindOptions (setFindOptions clears addSelect)
// Pass columnsToSelect so we only add columns that aren't already selected
commonQueryParser.addRelationOrderColumnsToBuilder(
queryBuilder,
parsedOrderBy,
flatObjectMetadata.nameSingular,
columnsToSelect,
);
const objectRecords = (await queryBuilder.getMany()) as ObjectRecord[];
@@ -142,17 +142,23 @@ export class GraphqlQueryParser {
queryBuilder: WorkspaceSelectQueryBuilder<any>,
parsedOrderBy: Record<string, OrderByClause>,
objectNameSingular: string,
columnsToSelect: Record<string, boolean>,
): void {
// Add relation ORDER BY columns with underscore alias for DISTINCT compatibility
// Add ORDER BY columns with underscore alias for DISTINCT compatibility
// This must be called AFTER setFindOptions because setFindOptions clears addSelect
// We need to add columns that are in orderBy but NOT in the selected columns
for (const orderByKey of Object.keys(parsedOrderBy)) {
const parts = orderByKey.split('.');
if (parts.length === 2) {
const [alias, column] = parts;
// Only add select for joined relation columns, not main entity columns
if (alias !== objectNameSingular) {
// For relation columns: always add (they're never in columnsToSelect)
// For main entity columns: only add if NOT already in columnsToSelect
const isMainEntity = alias === objectNameSingular;
const isAlreadySelected = isMainEntity && columnsToSelect[column];
if (!isAlreadySelected) {
queryBuilder.addSelect(
`"${alias}"."${column}"`,
`${alias}_${column}`,