diff --git a/src/Core/Models/GraphQLFilterParsers.cs b/src/Core/Models/GraphQLFilterParsers.cs index 153def832f..779ec64f33 100644 --- a/src/Core/Models/GraphQLFilterParsers.cs +++ b/src/Core/Models/GraphQLFilterParsers.cs @@ -419,10 +419,26 @@ private void HandleNestedFilterForSql( 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 - existsQuery.AddJoinPredicatesForRelatedEntity( - targetEntityName: queryStructure.EntityName, - relatedSourceAlias: queryStructure.SourceAlias, + // the outer table is added to the where clause of subquery. + // For self-referencing relationships (e.g., parent/child hierarchy), we need to use + // the relationship name to look up the correct foreign key definition. + // The parent query (queryStructure) calls AddJoinPredicatesForRelationship which adds + // predicates to the subquery (existsQuery), connecting queryStructure.SourceAlias to existsQuery.SourceAlias. + string relationshipName = filterField.Name; + EntityRelationshipKey fkLookupKey = new(queryStructure.EntityName, relationshipName); + + if (queryStructure is not BaseSqlQueryStructure sqlQueryStructure) + { + throw new DataApiBuilderException( + message: "Expected SQL query structure for nested filter processing.", + statusCode: HttpStatusCode.InternalServerError, + subStatusCode: DataApiBuilderException.SubStatusCodes.UnexpectedError); + } + + sqlQueryStructure.AddJoinPredicatesForRelationship( + fkLookupKey: fkLookupKey, + targetEntityName: nestedFilterEntityName, + subqueryTargetTableAlias: existsQuery.SourceAlias, subQuery: existsQuery); // The right operand is the SqlExistsQueryStructure. diff --git a/src/Service.Tests/SqlTests/GraphQLFilterTests/MsSqlGQLFilterTests.cs b/src/Service.Tests/SqlTests/GraphQLFilterTests/MsSqlGQLFilterTests.cs index 90e46940a9..ccc4e1efea 100644 --- a/src/Service.Tests/SqlTests/GraphQLFilterTests/MsSqlGQLFilterTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLFilterTests/MsSqlGQLFilterTests.cs @@ -355,6 +355,51 @@ WHERE [table3].[name] IN ('Aniruddh') await TestNestedFilterWithOrAndIN(existsPredicate, roleName: "authenticated"); } + /// + /// Test Nested Filter for Self-Referencing relationship + /// Tests that nested filters work correctly on self-referencing relationships (e.g., parent/child hierarchy). + /// Uses DimAccount table with parent_account relationship. + /// + [TestMethod] + public async Task TestNestedFilterSelfReferencing() + { + // This query should find all accounts whose parent account has AccountKey = 1 + // Expected to return account with AccountKey 2 (direct child of account 1) + string existsPredicate = $@" + EXISTS( SELECT 1 + FROM {GetPreIndentDefaultSchema()}[DimAccount] AS [table1] + WHERE [table1].[AccountKey] = 1 + AND [table0].[ParentAccountKey] = [table1].[AccountKey] )"; + + string graphQLQueryName = "dbo_DimAccounts"; + // Gets all the accounts that have a parent account with AccountKey = 1 + string gqlQuery = @"{ + dbo_DimAccounts (" + QueryBuilder.FILTER_FIELD_NAME + ": {" + + @"parent_account: { AccountKey: { eq: 1 }}}) + { + items { + AccountKey + ParentAccountKey + } + } + }"; + + string dbQuery = MakeQueryOn( + table: "DimAccount", + queriedColumns: new List { "AccountKey", "ParentAccountKey" }, + existsPredicate, + GetDefaultSchema(), + pkColumns: new List { "AccountKey" }); + + JsonElement actual = await ExecuteGraphQLRequestAsync( + gqlQuery, + graphQLQueryName, + isAuthenticated: false); + + string expected = await GetDatabaseResultAsync(dbQuery); + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual.ToString()); + } + /// /// Gets the default schema for /// MsSql.