Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 6, 2026

Why make this change?

Nested GraphQL filters on self-referencing relationships (e.g., parent/child hierarchies) return incorrect results. The filter fails to use the relationship name when looking up foreign key metadata, causing incorrect join predicates in the generated SQL.

What is this change?

Modified HandleNestedFilterForSql to use AddJoinPredicatesForRelationship instead of AddJoinPredicatesForRelatedEntity. The key difference:

Before:

existsQuery.AddJoinPredicatesForRelatedEntity(
    targetEntityName: queryStructure.EntityName,
    relatedSourceAlias: queryStructure.SourceAlias,
    subQuery: existsQuery);

After:

string relationshipName = filterField.Name;
EntityRelationshipKey fkLookupKey = new(queryStructure.EntityName, relationshipName);
sqlQueryStructure.AddJoinPredicatesForRelationship(
    fkLookupKey: fkLookupKey,
    targetEntityName: nestedFilterEntityName,
    subqueryTargetTableAlias: existsQuery.SourceAlias,
    subQuery: existsQuery);

The EntityRelationshipKey includes both entity name and relationship name, enabling correct foreign key lookup for self-referencing entities where multiple relationships exist (e.g., parent_account vs child_accounts on DimAccount).

How was this tested?

  • Integration Tests
  • Unit Tests

Sample Request(s)

Query for accounts whose parent has AccountKey = 1:

{
  dbo_DimAccounts(filter: { parent_account: { AccountKey: { eq: 1 }}}) {
    items {
      AccountKey
      ParentAccountKey
    }
  }
}

With test data:

  • Account 1: ParentAccountKey = null
  • Account 2: ParentAccountKey = 1
  • Account 3: ParentAccountKey = 2
  • Account 4: ParentAccountKey = 2

Returns account 2 (the direct child of account 1).

Original prompt

This section details on the original issue you should resolve

<issue_title>[Bug]: Nested filter on Self-Referencing Relationships returns incorrect results</issue_title>
<issue_description>### What happened?

Problem

When using GraphQL nested filters on self-referencing relationships (e.g., parent/child hierarchy), the filter returns incorrect results.

This query gives incorrect results

query {
  books(filter: { category: { parent: { name: { contains: "Classic" } } } }) {
    items {
      id
      category {
        name
        parent {
          name
        }
      }
    }
  }
}

Expected Behavior

Returns book items with categories whose parent's name contains "Classic".

Proposed Solution

  1. In HandleNestedFilterForSql, use AddJoinPredicatesForRelationship instead of AddJoinPredicatesForRelatedEntity
  2. Create an EntityRelationshipKey using the relationship name (filter field name) to look up the correct FK definition
  3. Call the method on the parent query structure (not the EXISTS subquery) with the correct parameters:
    • fkLookupKey: {queryStructure.EntityName, filterField.Name}
    • targetEntityName: the nested filter entity name
    • subqueryTargetTableAlias: the EXISTS subquery's source alias

In BaseGraphQLFilterParsers.cs:

/// <summary>
/// For SQL, a nested filter represents an EXISTS clause with a join between
/// the parent entity being filtered and the related entity representing the
/// non-scalar filter input. This function:
/// 1. Defines the Exists Query structure
/// 2. Recursively parses any more(possibly nested) filters on the Exists sub query.
/// 3. Adds join predicates between the related entities to the Exists sub query.
/// 4. Adds the Exists subquery to the existing list of predicates.
/// </summary>
/// <param name="ctx">The middleware context</param>
/// <param name="filterField">The nested filter field.</param>
/// <param name="subfields">The subfields of the nested filter.</param>
/// <param name="predicates">The predicates parsed so far.</param>
/// <param name="queryStructure">The query structure of the entity being filtered.</param>
/// <exception cref="DataApiBuilderException">
/// throws if a relationship directive is not found on the nested filter input</exception>
private void HandleNestedFilterForSql(
    IMiddlewareContext ctx,
    InputField filterField,
    List<ObjectFieldNode> subfields,
    List<PredicateOperand> predicates,
    BaseQueryStructure queryStructure,
    ISqlMetadataProvider metadataProvider)
{
    string? targetGraphQLTypeNameForFilter = RelationshipDirectiveType.GetTarget(filterField);

    if (targetGraphQLTypeNameForFilter is null)
    {
        throw new DataApiBuilderException(
            message: "The GraphQL schema is missing the relationship directive on input field.",
            statusCode: HttpStatusCode.InternalServerError,
            subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError);
    }

    string nestedFilterEntityName = metadataProvider.GetEntityName(targetGraphQLTypeNameForFilter);

    // Validate that the field referenced in the nested input filter can be accessed.
    bool entityAccessPermitted = queryStructure.AuthorizationResolver.AreRoleAndOperationDefinedForEntity(
        entityIdentifier: nestedFilterEntityName,
        roleName: GetHttpContextFromMiddlewareContext(ctx).Request.Headers[CLIENT_ROLE_HEADER].ToString(),
        operation: EntityActionOperation.Read);

    if (!entityAccessPermitted)
    {
        throw new DataApiBuilderException(
            message: DataApiBuilderException.GRAPHQL_FILTER_ENTITY_AUTHZ_FAILURE,
            statusCode: HttpStatusCode.Forbidden,
            subStatusCode: DataApiBuilderException.SubStatusCodes.AuthorizationCheckFailed);
    }

    List<Predicate> predicatesForExistsQuery = new();

    // Create an SqlExistsQueryStructure as the predicate operand of Exists predicate
    // This query structure has no order by, no limit and selects 1
    // its predicates are obtained from recursively parsing the nested filter
    // and an additional predicate to reflect the join between main query and this exists subquery.
    SqlExistsQueryStructure existsQuery = new(
        GetHttpContextFromMiddlewareContext(ctx),
        metadataProvider,
        queryStructure.AuthorizationResolver,
        this,
        predicatesForExistsQuery,
        nestedFilterEntityName,
        queryStructure.Counter);

    // Recursively parse and obtain the predicates for the Exists clause subquery
    Predicate existsQueryFilterPredicate = Parse(ctx,
            filterField,
            subfields,
            existsQuery);
    predicatesForExistsQuery.Push(existsQueryFilterPredicate);

    // Add JoinPredicates to the subquery query structure so a predicate connecting
    // the outer table is added to the where clause of subquery.
    // For self-referencing relationships (e.g., parent/child hierarchy),...

</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes Azure/data-api-builder#3028

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey).

Copilot AI and others added 2 commits January 6, 2026 00:15
…ion test

Co-authored-by: Aniruddh25 <3513779+Aniruddh25@users.noreply.github.com>
Co-authored-by: Aniruddh25 <3513779+Aniruddh25@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix nested filter on self-referencing relationships Fix nested filter on self-referencing relationships Jan 6, 2026
Copilot AI requested a review from Aniruddh25 January 6, 2026 00:25
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