From 332e5702ada2ce1795e87005ff0d63923ad0ff5f Mon Sep 17 00:00:00 2001 From: hippi345 <13539685+hippi345@users.noreply.github.com> Date: Sat, 17 Oct 2026 01:22:42 +0000 Subject: [PATCH 1/3] Fix GraphQL fragment selection dropping trailing fields AddGraphQLFields used return after expanding a fragment spread or inline fragment, which exited the selection loop and omitted later fields. Continue to the next selection instead. --- .../Sql Query Structures/SqlQueryStructure.cs | 4 +- .../GraphQLQueryTests/GraphQLQueryTestBase.cs | 51 ++++++++++++++++++ .../UnitTests/SqlQueryStructureHelperTests.cs | 54 ++++++++++++++++++- 3 files changed, 105 insertions(+), 4 deletions(-) diff --git a/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs b/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs index 26e7a020d5..f14defe612 100644 --- a/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs +++ b/src/Core/Resolvers/Sql Query Structures/SqlQueryStructure.cs @@ -775,14 +775,14 @@ private void AddGraphQLFields(IReadOnlyList selections, RuntimeC .First(); AddGraphQLFields(fragmentDocumentNode.SelectionSet.Selections, runtimeConfigProvider); - return; + continue; } if (node.Kind == SyntaxKind.InlineFragment) { InlineFragmentNode inlineFragment = (InlineFragmentNode)node; AddGraphQLFields(inlineFragment.SelectionSet.Selections, runtimeConfigProvider); - return; + continue; } if (node.Kind != SyntaxKind.Field) diff --git a/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs b/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs index 16c1a878fa..e29faa79d2 100644 --- a/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs @@ -1666,6 +1666,57 @@ fragment p on book { id } Assert.AreEqual(1, response.GetProperty("id").GetInt32()); } + /// + /// Fields listed after a fragment spread in the same selection set must still be + /// resolved. Previously AddGraphQLFields returned after expanding a fragment, + /// which dropped trailing scalar and relationship fields. + /// + [TestMethod] + public async Task QueryWithFragmentFollowedByTrailingFields() + { + string query = @" +query { + book_by_pk(id: 1) { + ...bookFields + title + publishers { name } + } +} + +fragment bookFields on book { id } + "; + + JsonElement response = await ExecuteGraphQLRequestAsync(query, "book_by_pk", false); + + Assert.AreEqual(1, response.GetProperty("id").GetInt32()); + Assert.AreEqual("Awesome book", response.GetProperty("title").GetString()); + Assert.AreEqual("Big Company", response.GetProperty("publishers").GetProperty("name").GetString()); + } + + /// + /// Fields listed after an inline fragment in the same selection set must still be + /// resolved. Previously AddGraphQLFields returned after expanding an inline fragment. + /// + [TestMethod] + public async Task QueryWithInlineFragmentFollowedByTrailingFields() + { + string query = @" +query { + book_by_pk(id: 1) { + ... on book { id } + title + publishers { name } + } +} + "; + + JsonElement response = await ExecuteGraphQLRequestAsync(query, "book_by_pk", false); + + Assert.AreEqual(1, response.GetProperty("id").GetInt32()); + Assert.AreEqual("Awesome book", response.GetProperty("title").GetString()); + Assert.AreEqual("Big Company", response.GetProperty("publishers").GetProperty("name").GetString()); + } + [TestMethod] public async Task GraphQLQueryWithMultipleOfTheSameFieldReturnsFieldOnce() { diff --git a/src/Service.Tests/UnitTests/SqlQueryStructureHelperTests.cs b/src/Service.Tests/UnitTests/SqlQueryStructureHelperTests.cs index ee35167962..4e781ea7fa 100644 --- a/src/Service.Tests/UnitTests/SqlQueryStructureHelperTests.cs +++ b/src/Service.Tests/UnitTests/SqlQueryStructureHelperTests.cs @@ -1,11 +1,15 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; +using Azure.DataApiBuilder.Config.DatabasePrimitives; using Azure.DataApiBuilder.Core.Models; using Azure.DataApiBuilder.Core.Resolvers; +using Azure.DataApiBuilder.Core.Services; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.GraphQLBuilder.Queries; using HotChocolate.Language; @@ -127,6 +131,31 @@ public void AddGraphQLFields_InlineFragmentSkipsIntrospectionField() new object?[] { new ISelectionNode[] { fragment }, null }); } + /// + /// Selections after an inline fragment must still be processed. Using return after + /// expanding the fragment exits the foreach and drops trailing fields. + /// + [TestMethod] + public void AddGraphQLFields_ContinuesAfterInlineFragment() + { + SqlQueryStructure structure = CreateStructureForColumnSelection(); + InlineFragmentNode fragment = new( + location: null, + typeCondition: new NamedTypeNode("book"), + directives: System.Array.Empty(), + selectionSet: new SelectionSetNode(new ISelectionNode[] { new FieldNode("id") })); + + GetPrivateMethod("AddGraphQLFields").Invoke( + structure, + new object?[] + { + new ISelectionNode[] { fragment, new FieldNode("title") }, + null + }); + + CollectionAssert.AreEquivalent(new[] { "id", "title" }, structure.Columns.Select(c => c.Label).ToList()); + } + [TestMethod] public void AddGraphQLFields_UnsupportedSelectionThrows() { @@ -175,9 +204,30 @@ private static SqlQueryStructure CreateStructure() return structure; } - private static void SetAutoProperty(SqlQueryStructure structure, string propertyName, T value) + private static SqlQueryStructure CreateStructureForColumnSelection() + { + SqlQueryStructure structure = CreateStructure(); + Mock metadata = new(); + metadata + .Setup(m => m.TryGetBackingColumn(It.IsAny(), It.IsAny(), out It.Ref.IsAny)) + .Returns((string _, string field, out string? backing) => + { + backing = field; + return true; + }); + + SetAutoProperty(structure, "MetadataProvider", metadata.Object, typeof(BaseQueryStructure)); + SetAutoProperty(structure, "DatabaseObject", new DatabaseTable("public", "books"), typeof(BaseQueryStructure)); + SetAutoProperty(structure, "Columns", new List(), typeof(BaseQueryStructure)); + structure.EntityName = "book"; + structure.SourceAlias = "table0"; + return structure; + } + + private static void SetAutoProperty(SqlQueryStructure structure, string propertyName, T value, Type? declaringType = null) { - typeof(SqlQueryStructure).GetField($"<{propertyName}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic)! + Type type = declaringType ?? typeof(SqlQueryStructure); + type.GetField($"<{propertyName}>k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic)! .SetValue(structure, value); } From 656a578762e640041607d3b4fa990b76367ee256 Mon Sep 17 00:00:00 2001 From: hippi345 <13539685+hippi345@users.noreply.github.com> Date: Sat, 17 Oct 2026 01:25:46 +0000 Subject: [PATCH 2/3] Remove GraphQL integration tests that pass on current main HTTP fragment queries do not hit AddGraphQLFields' fragment branches on Hot Chocolate 16, so keep only the unit test that covers the loop. --- .../GraphQLQueryTests/GraphQLQueryTestBase.cs | 51 ------------------- 1 file changed, 51 deletions(-) diff --git a/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs b/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs index e29faa79d2..16c1a878fa 100644 --- a/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLQueryTests/GraphQLQueryTestBase.cs @@ -1666,57 +1666,6 @@ fragment p on book { id } Assert.AreEqual(1, response.GetProperty("id").GetInt32()); } - /// - /// Fields listed after a fragment spread in the same selection set must still be - /// resolved. Previously AddGraphQLFields returned after expanding a fragment, - /// which dropped trailing scalar and relationship fields. - /// - [TestMethod] - public async Task QueryWithFragmentFollowedByTrailingFields() - { - string query = @" -query { - book_by_pk(id: 1) { - ...bookFields - title - publishers { name } - } -} - -fragment bookFields on book { id } - "; - - JsonElement response = await ExecuteGraphQLRequestAsync(query, "book_by_pk", false); - - Assert.AreEqual(1, response.GetProperty("id").GetInt32()); - Assert.AreEqual("Awesome book", response.GetProperty("title").GetString()); - Assert.AreEqual("Big Company", response.GetProperty("publishers").GetProperty("name").GetString()); - } - - /// - /// Fields listed after an inline fragment in the same selection set must still be - /// resolved. Previously AddGraphQLFields returned after expanding an inline fragment. - /// - [TestMethod] - public async Task QueryWithInlineFragmentFollowedByTrailingFields() - { - string query = @" -query { - book_by_pk(id: 1) { - ... on book { id } - title - publishers { name } - } -} - "; - - JsonElement response = await ExecuteGraphQLRequestAsync(query, "book_by_pk", false); - - Assert.AreEqual(1, response.GetProperty("id").GetInt32()); - Assert.AreEqual("Awesome book", response.GetProperty("title").GetString()); - Assert.AreEqual("Big Company", response.GetProperty("publishers").GetProperty("name").GetString()); - } - [TestMethod] public async Task GraphQLQueryWithMultipleOfTheSameFieldReturnsFieldOnce() { From 8c835179d37f054601094c67b095982b0eacce50 Mon Sep 17 00:00:00 2001 From: hippi345 <13539685+hippi345@users.noreply.github.com> Date: Mon, 26 Oct 2026 14:20:42 +0000 Subject: [PATCH 3/3] Add named fragment regression for trailing GraphQL fields Cover the #3861 query shape: a bookFields spread selecting id, followed by title. The test fails if AddGraphQLFields returns after the spread, because the trailing field is omitted from the query columns. --- .../UnitTests/SqlQueryStructureHelperTests.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/Service.Tests/UnitTests/SqlQueryStructureHelperTests.cs b/src/Service.Tests/UnitTests/SqlQueryStructureHelperTests.cs index 4e781ea7fa..88fdc2412b 100644 --- a/src/Service.Tests/UnitTests/SqlQueryStructureHelperTests.cs +++ b/src/Service.Tests/UnitTests/SqlQueryStructureHelperTests.cs @@ -13,6 +13,7 @@ using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.GraphQLBuilder.Queries; using HotChocolate.Language; +using HotChocolate.Resolvers; using Microsoft.AspNetCore.Http; using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; @@ -156,6 +157,52 @@ public void AddGraphQLFields_ContinuesAfterInlineFragment() CollectionAssert.AreEquivalent(new[] { "id", "title" }, structure.Columns.Select(c => c.Label).ToList()); } + /// + /// Selections after a named fragment spread must still be processed. + /// This matches the #3861 query: ...bookFields (selecting id) followed by title. + /// Using return after expanding the spread exits the foreach and drops the trailing field. + /// + [TestMethod] + public void AddGraphQLFields_ContinuesAfterNamedFragmentSpread() + { + const string query = """ + query { + book_by_pk(id: 1) { + ...bookFields + title + } + } + + fragment bookFields on book { id } + """; + + DocumentNode document = Utf8GraphQLParser.Parse(query); + OperationDefinitionNode operationDefinition = document.Definitions.OfType().Single(); + FieldNode bookByPk = operationDefinition.SelectionSet.Selections.OfType().Single(); + + // Operation.Document is a non-virtual property on a sealed type, so the document + // is assigned the same way other uninitialized query-structure state is in this fixture. + // The name is fully qualified because Azure.Core also defines Azure.Operation. + HotChocolate.Execution.Processing.Operation graphqlOperation = + (HotChocolate.Execution.Processing.Operation)RuntimeHelpers.GetUninitializedObject( + typeof(HotChocolate.Execution.Processing.Operation)); + typeof(HotChocolate.Execution.Processing.Operation) + .GetField("k__BackingField", BindingFlags.Instance | BindingFlags.NonPublic)! + .SetValue(graphqlOperation, document); + Mock context = new(); + context.SetupGet(ctx => ctx.Operation).Returns(graphqlOperation); + + SqlQueryStructure structure = CreateStructureForColumnSelection(); + typeof(SqlQueryStructure).GetField("_ctx", BindingFlags.Instance | BindingFlags.NonPublic)! + .SetValue(structure, context.Object); + + GetPrivateMethod("AddGraphQLFields").Invoke( + structure, + new object?[] { bookByPk.SelectionSet!.Selections, null }); + + CollectionAssert.AreEquivalent(new[] { "id", "title" }, structure.Columns.Select(c => c.Label).ToList()); + } + [TestMethod] public void AddGraphQLFields_UnsupportedSelectionThrows() {