Skip to content

fix(mysql): apply unique table alias on self-referential relationship subqueries#97

Merged
GavinRay97 merged 1 commit into
mainfrom
contrib/qamar-mysql-self-join-alias
May 5, 2026
Merged

fix(mysql): apply unique table alias on self-referential relationship subqueries#97
GavinRay97 merged 1 commit into
mainfrom
contrib/qamar-mysql-self-join-alias

Conversation

@GavinRay97

Copy link
Copy Markdown
Member

Re-opened from #96 on an upstream branch so paid-license CI secrets (JOOQ_PRO_*) are injected — GitHub Actions strips org/repo secrets from pull_request runs originating in forks. All commits authored by @qamar-hashmi; full credit retained in git log.

Problem

When a relationship's source and target are the same collection (e.g. regions.parentRegionId → regions.id), the correlated subquery was aliased with the same name as the outer table scan. MySQL resolved both sides of the JOIN condition to the inner scope, turning:

WHERE regions.parentRegionId = regions.id

into a tautological self-comparison that always returned no rows — with no error surfaced in GraphQL or connector logs.

Note: PR #88 fixed the same issue for Snowflake and Trino (CTEQueryGenerator.kt). This PR applies the equivalent fix to MySQL (JSONGenerator.kt).

Fix

Derive a unique inner alias from the field alias and target collection name (e.g. parentRegion_regions) and pass it through to:

  • The FROM clause alias (FROM regions AS parentRegion_regions)
  • mkJoinWhereClause (WHERE regions.FK = parentRegion_regions.PK)
  • getSelectOrderFields / getConcatOrderFields / translateIROrderByField
  • expressionToCondition (predicate field references inside the subquery)
  • collectRequiredJoinTablesForWhereClause rootTable arg
  • The derived-table alias passed to asTable()

Cross-table relationships are unaffected — existing behaviour is preserved by the defaulted innerAlias parameter on mkJoinWhereClause.

Test

Adds ndc-ir/src/test/resources/queryRequests/self_referential_object_relationship.json — a query fixture for the self-referential object relationship case (regions → parentRegion via parentRegionId → id on the same table).

Verification

Verified end-to-end in a local DDN project:

  • Built patched image via multi-stage Dockerfile (kotlinc 2.0.21 against classpath from ghcr.io/hasura/ndc-jvm-mysql:v1.0.18)
  • Queried { regions { id name parentRegionId parentRegion { id name } } } — all non-root rows resolved parentRegion correctly (previously all returned null)
  • No regressions on cross-table relationships

… subqueries

When a relationship's source and target are the same collection (e.g.
regions.parentRegionId → regions.id), the correlated subquery was aliased
with the same name as the outer table scan. MySQL resolved both sides of
the JOIN condition to the inner scope, turning:

  WHERE regions.parentRegionId = regions.id

…into a tautological self-comparison that always returned no rows, with
no error surfaced in GraphQL or connector logs.

Fix: derive a unique inner alias from the field alias and target
collection name (e.g. "parentRegion_regions") and pass it through to:
  - the FROM clause alias (FROM regions AS parentRegion_regions)
  - mkJoinWhereClause (WHERE regions.FK = parentRegion_regions.PK)
  - getSelectOrderFields / getConcatOrderFields / translateIROrderByField
  - expressionToCondition (predicate field references inside the subquery)
  - collectRequiredJoinTablesForWhereClause rootTable arg
  - the derived-table alias passed to asTable()

Cross-table relationships are unaffected: the inner alias always differs
from the outer table name, and existing behaviour for non-self-joins is
preserved by the defaulted innerAlias parameter on mkJoinWhereClause.

Adds a query-request fixture for the self-referential object relationship
case (regions → parentRegion via parentRegionId → id).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@GavinRay97 GavinRay97 merged commit 631d374 into main May 5, 2026
2 of 9 checks passed
GavinRay97 added a commit to hasura/ndc-hub that referenced this pull request May 6, 2026
Bumps the MySQL connector to **v1.0.19**.

> Re-opened from #742 on an upstream branch — fork PRs fail CI because
GitHub Actions strips repo/org secrets from `pull_request` runs
originating in forks.

## Changes

Picks up
[hasura/ndc-jvm-mono#97](hasura/ndc-jvm-mono#97)
— *fix(mysql): apply unique table alias on self-referential relationship
subqueries*.

When a relationship's source and target are the same collection (e.g.
`regions.parentRegionId → regions.id`), the correlated subquery was
previously aliased with the same name as the outer table scan. MySQL
resolved both sides of the join condition to the inner scope, producing
a tautological self-comparison that silently returned no rows.

Equivalent fix to [#88](hasura/ndc-jvm-mono#88)
(Snowflake/Trino) applied to MySQL's `JSONGenerator.kt`.

## Release artifacts

- Tag:
[`mysql/v1.0.19`](https://github.com/hasura/ndc-jvm-mono/releases/tag/mysql/v1.0.19)
- Image: `ghcr.io/hasura/ndc-jvm-mysql:v1.0.19` (linux/amd64,
linux/arm64)
- CLI: `ghcr.io/hasura/ndc-jvm-cli:v1.0.19-mysql`
- Source commit: `631d37452f26c736ed073c04bdce37d55bbff814`
- Tarball sha256:
`abfaf09c97327fe5d6bd1e98ba4df2c6d6fd5577c5f60d8b00d3ee10f8b28af0`

Original contributor: @qamar-hashmi.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
dliub pushed a commit to dliub/ndc-jvm-mono that referenced this pull request May 7, 2026
…predicates

PR hasura#97 (v1.0.19) introduced a unique inner alias for self-referential subqueries
to fix self-join correlation. The alias was threaded through expressionToCondition
via the existing 3-arg overload, which does request.copy(collection = alias). That
single field on QueryRequest is consumed for two distinct purposes downstream:

  1. SQL table qualification of empty-path columns (correct: must use alias)
  2. Connector-config lookup in BaseGenerator.getColumnType (incorrect: must
     use the real collection name)

When a relationship's WHERE filter referenced a column on the aliased subquery
table, getColumnType received the alias and threw:

  IllegalStateException: Collection products_products not found in connector
  configuration

This affected nested cross-table relationships with WHERE filters as well as the
intended self-join + WHERE case.

Fix: introduce expressionToConditionWithSqlAlias on BaseGenerator. It accepts a
QueryRequest whose .collection is the real collection name (used for config
lookup) plus a separate sqlTableAlias used only for SQL table qualification of
empty-path columns. The alias propagates through And/Or/Not recursion and into
the outer-table qualifier of EXISTS join conditions, but is reset to null inside
the EXISTS predicate (which switches collection context). Existing 2-arg and
3-arg expressionToCondition overloads are preserved for callers that want a
fully real-collection context.

JSONGenerator.kt swaps the buggy expressionToCondition(predicate, request,
effectiveAlias) for expressionToConditionWithSqlAlias(predicate, request,
effectiveAlias). request.collection now stays as the real collection name.

Adds unit tests in ndc-sqlgen covering: empty-path predicate qualifies SQL with
alias while config lookup uses the real collection, default 2-arg behaviour is
unchanged, the regression no longer throws, alias propagates through compound
predicates. Tests load a fixture configuration.json via HASURA_CONFIGURATION_DIRECTORY
set on the Gradle test task.

Adds JSON query-request fixtures for the four reported regression scenarios:
self-join with WHERE, single-level cross-table with WHERE, nested cross-table
with WHERE (the original bug report), and deep mixed self-join + cross-table
with WHERE.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants