Skip to content
24 changes: 20 additions & 4 deletions src/Core/Models/GraphQLFilterParsers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -420,10 +420,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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,51 @@ WHERE [table3].[name] IN ('Aniruddh')
await TestNestedFilterWithOrAndIN(existsPredicate, roleName: "authenticated");
}

/// <summary>
/// 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.
/// </summary>
[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<string> { "AccountKey", "ParentAccountKey" },
existsPredicate,
GetDefaultSchema(),
pkColumns: new List<string> { "AccountKey" });

JsonElement actual = await ExecuteGraphQLRequestAsync(
gqlQuery,
graphQLQueryName,
isAuthenticated: false);

string expected = await GetDatabaseResultAsync(dbQuery);
SqlTestHelper.PerformTestEqualJsonStrings(expected, actual.ToString());
}

/// <summary>
/// Gets the default schema for
/// MsSql.
Expand Down