Skip to content

Fix nested filter on self-referencing relationships#3038

Open
Copilot wants to merge 6 commits intomainfrom
copilot/fix-nested-filter-bug
Open

Fix nested filter on self-referencing relationships#3038
Copilot wants to merge 6 commits intomainfrom
copilot/fix-nested-filter-bug

Conversation

Copy link
Contributor

Copilot AI commented Jan 6, 2026

Fix Nested Filter on Self-Referencing Relationships

Completed Tasks:

  • Understand the issue: Current code uses AddJoinPredicatesForRelatedEntity which doesn't handle self-referencing relationships correctly
  • Modify HandleNestedFilterForSql in GraphQLFilterParsers.cs to use AddJoinPredicatesForRelationship instead
  • Create an EntityRelationshipKey using the relationship name (filter field name)
  • Call AddJoinPredicatesForRelationship on the parent query structure with correct parameters
  • Add safe type checking before casting to BaseSqlQueryStructure
  • Add integration test for self-referencing relationship filter in MsSqlGQLFilterTests.cs
  • Test uses DimAccount table with parent_account relationship
  • Build and verify the changes compile correctly
  • Run code review and security checks - all passed
  • Fix formatting issue: Remove trailing whitespace from blank line

Changes Summary:

  1. GraphQLFilterParsers.cs: Modified HandleNestedFilterForSql to properly handle self-referencing relationships by using the relationship name to look up foreign key definitions
  2. MsSqlGQLFilterTests.cs: Added TestNestedFilterSelfReferencing integration test to validate the fix
  3. Formatting: Removed trailing whitespace to pass CI formatting checks

Security Summary:

✅ CodeQL scan completed with 0 alerts
✅ Code review completed with only minor style nitpicks
✅ All builds successful
✅ Formatting verified with dotnet format --verify-no-changes

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 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
@JerryNixon JerryNixon added the 2.0 label Feb 4, 2026
@Aniruddh25 Aniruddh25 marked this pull request as ready for review March 12, 2026 02:23
Copilot AI review requested due to automatic review settings March 12, 2026 02:23
@Aniruddh25
Copy link
Collaborator

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 6 pipeline(s).

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends GraphQL nested-filter handling to correctly support self-referencing relationships (e.g., parent/child hierarchies) for SQL-backed entities, and adds a regression test for the scenario on MSSQL.

Changes:

  • Update SQL nested-filter join predicate generation to use relationship-name-based FK lookup (supports self-joins).
  • Add an MSSQL integration test validating nested filters on the DimAccount self-referencing relationship.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/Service.Tests/SqlTests/GraphQLFilterTests/MsSqlGQLFilterTests.cs Adds a new MSSQL test covering nested filtering through a self-referencing relationship (parent_account).
src/Core/Models/GraphQLFilterParsers.cs Adjusts nested-filter SQL join predicate creation to use relationship-aware FK lookup, enabling self-referencing relationship filters.

Copilot AI requested a review from Aniruddh25 March 12, 2026 02:33
@Aniruddh25 Aniruddh25 enabled auto-merge (squash) March 12, 2026 02:38
@Aniruddh25
Copy link
Collaborator

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 6 pipeline(s).

@Aniruddh25
Copy link
Collaborator

/azp run

@azure-pipelines
Copy link

Azure Pipelines successfully started running 6 pipeline(s).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants