From 72122c5cabc815c681e103ebe21ac3c44b023e84 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 21:54:39 +0000 Subject: [PATCH 1/3] Initial plan From 594f24760c34af411981cd07fdfba576667f0439 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 22:16:02 +0000 Subject: [PATCH 2/3] Fix GraphQL aggregation features disabled when runtime.graphql section is absent - Fix RuntimeConfig.EnableAggregation to use consistent OR logic (returns true when Runtime is null or Runtime.GraphQL is null, matching IsGraphQLEnabled pattern) - Fix GraphQLSchemaCreator.OnConfigChanged to update _isAggregationEnabled on hot-reload - Add enable-aggregation to dab.draft.schema.json (runtime.graphql section) - Add unit tests: EnableAggregation defaults to true when graphql/runtime section absent, can be explicitly disabled; QueryBuilder adds groupBy to MSSQL connection type but not PostgreSQL Agent-Logs-Url: https://github.com/Azure/data-api-builder/sessions/b4c70fb5-8795-4c8a-a904-d54344395c93 Co-authored-by: Aniruddh25 <3513779+Aniruddh25@users.noreply.github.com> --- schemas/dab.draft.schema.json | 5 + src/Config/ObjectModel/RuntimeConfig.cs | 8 +- src/Core/Services/GraphQLSchemaCreator.cs | 4 +- .../Configuration/RuntimeConfigLoaderTests.cs | 155 ++++++++++++++++++ .../GraphQLBuilder/QueryBuilderTests.cs | 113 +++++++++++++ 5 files changed, 281 insertions(+), 4 deletions(-) diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index 4561b06952..25dd39bc95 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -244,6 +244,11 @@ "description": "Maximum allowed depth of a GraphQL query.", "default": null }, + "enable-aggregation": { + "$ref": "#/$defs/boolean-or-string", + "description": "Allow enabling/disabling aggregation (groupBy, sum, avg, min, max, count) for supported database types (MSSQL, DWSQL).", + "default": true + }, "multiple-mutations": { "type": "object", "description": "Configuration properties for multiple mutation operations", diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index d6bdefc1b8..5d18ec75da 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -194,12 +194,14 @@ Runtime.GraphQL is null || public string DefaultDataSourceName { get; set; } /// - /// Retrieves the value of runtime.graphql.aggregation.enabled property if present, default is true. + /// Retrieves the value of runtime.graphql.enable-aggregation property if present, default is true. + /// Returns true when runtime section is absent, when graphql section is absent, + /// or when enable-aggregation is explicitly set to true. /// [JsonIgnore] public bool EnableAggregation => - Runtime is not null && - Runtime.GraphQL is not null && + Runtime is null || + Runtime.GraphQL is null || Runtime.GraphQL.EnableAggregation; [JsonIgnore] diff --git a/src/Core/Services/GraphQLSchemaCreator.cs b/src/Core/Services/GraphQLSchemaCreator.cs index 90e918c833..229b8b0855 100644 --- a/src/Core/Services/GraphQLSchemaCreator.cs +++ b/src/Core/Services/GraphQLSchemaCreator.cs @@ -80,12 +80,14 @@ public GraphQLSchemaCreator( /// /// Executed when a hot-reload event occurs. Pulls the latest /// runtimeconfig object from the provider and updates the flag indicating - /// whether multiple create operations are enabled, and the entities based on the new config. + /// whether multiple create operations are enabled, whether aggregation is enabled, + /// and the entities based on the new config. /// protected void OnConfigChanged(object? sender, HotReloadEventArgs args) { RuntimeConfig runtimeConfig = _runtimeConfigProvider.GetConfig(); _isMultipleCreateOperationEnabled = runtimeConfig.IsMultipleCreateOperationEnabled(); + _isAggregationEnabled = runtimeConfig.EnableAggregation; _entities = runtimeConfig.Entities; } diff --git a/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs b/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs index d724753f97..024c84c409 100644 --- a/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs +++ b/src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs @@ -283,4 +283,159 @@ public async Task ChildConfigLoadFailureHaltsParentConfigLoading() } } } + + /// + /// Tests that EnableAggregation returns true by default when runtime.graphql section is absent. + /// This is a regression test for the bug where EnableAggregation returned false (disabled) + /// when Runtime.GraphQL was null, even though the default value for EnableAggregation is true. + /// + [TestMethod] + public void EnableAggregation_WhenGraphQLSectionAbsent_DefaultsToTrue() + { + // Arrange: a minimal config with no runtime.graphql section + string configJson = @"{ + ""$schema"": ""https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch/dab.draft.schema.json"", + ""data-source"": { + ""database-type"": ""mssql"", + ""connection-string"": ""Server=tcp:127.0.0.1,1433;"" + }, + ""runtime"": { + ""host"": { + ""authentication"": { ""provider"": ""StaticWebApps"" } + } + }, + ""entities"": {} + }"; + + IFileSystem fs = new MockFileSystem(new Dictionary() + { + { "dab-config.json", new MockFileData(configJson) } + }); + + FileSystemRuntimeConfigLoader loader = new(fs); + + // Act + bool loaded = loader.TryLoadConfig("dab-config.json", out RuntimeConfig runtimeConfig); + + // Assert + Assert.IsTrue(loaded, "Config should load successfully."); + Assert.IsNull(runtimeConfig.Runtime?.GraphQL, "GraphQL section should be null for this config."); + Assert.IsTrue(runtimeConfig.EnableAggregation, + "EnableAggregation should default to true when runtime.graphql section is absent."); + } + + /// + /// Tests that EnableAggregation returns true by default when runtime section is absent. + /// + [TestMethod] + public void EnableAggregation_WhenRuntimeSectionAbsent_DefaultsToTrue() + { + // Arrange: a minimal config with no runtime section at all + string configJson = @"{ + ""$schema"": ""https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch/dab.draft.schema.json"", + ""data-source"": { + ""database-type"": ""mssql"", + ""connection-string"": ""Server=tcp:127.0.0.1,1433;"" + }, + ""entities"": {} + }"; + + IFileSystem fs = new MockFileSystem(new Dictionary() + { + { "dab-config.json", new MockFileData(configJson) } + }); + + FileSystemRuntimeConfigLoader loader = new(fs); + + // Act + bool loaded = loader.TryLoadConfig("dab-config.json", out RuntimeConfig runtimeConfig); + + // Assert + Assert.IsTrue(loaded, "Config should load successfully."); + Assert.IsNull(runtimeConfig.Runtime, "Runtime section should be null for this config."); + Assert.IsTrue(runtimeConfig.EnableAggregation, + "EnableAggregation should default to true when runtime section is absent."); + } + + /// + /// Tests that EnableAggregation returns false when explicitly disabled in config. + /// + [TestMethod] + public void EnableAggregation_WhenExplicitlyDisabled_ReturnsFalse() + { + // Arrange: a config with enable-aggregation explicitly set to false + string configJson = @"{ + ""$schema"": ""https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch/dab.draft.schema.json"", + ""data-source"": { + ""database-type"": ""mssql"", + ""connection-string"": ""Server=tcp:127.0.0.1,1433;"" + }, + ""runtime"": { + ""graphql"": { + ""enabled"": true, + ""enable-aggregation"": false + }, + ""host"": { + ""authentication"": { ""provider"": ""StaticWebApps"" } + } + }, + ""entities"": {} + }"; + + IFileSystem fs = new MockFileSystem(new Dictionary() + { + { "dab-config.json", new MockFileData(configJson) } + }); + + FileSystemRuntimeConfigLoader loader = new(fs); + + // Act + bool loaded = loader.TryLoadConfig("dab-config.json", out RuntimeConfig runtimeConfig); + + // Assert + Assert.IsTrue(loaded, "Config should load successfully."); + Assert.IsFalse(runtimeConfig.EnableAggregation, + "EnableAggregation should be false when explicitly set to false in config."); + } + + /// + /// Tests that EnableAggregation returns true when explicitly enabled in config. + /// + [TestMethod] + public void EnableAggregation_WhenExplicitlyEnabled_ReturnsTrue() + { + // Arrange: a config with enable-aggregation explicitly set to true + string configJson = @"{ + ""$schema"": ""https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch/dab.draft.schema.json"", + ""data-source"": { + ""database-type"": ""mssql"", + ""connection-string"": ""Server=tcp:127.0.0.1,1433;"" + }, + ""runtime"": { + ""graphql"": { + ""enabled"": true, + ""enable-aggregation"": true + }, + ""host"": { + ""authentication"": { ""provider"": ""StaticWebApps"" } + } + }, + ""entities"": {} + }"; + + IFileSystem fs = new MockFileSystem(new Dictionary() + { + { "dab-config.json", new MockFileData(configJson) } + }); + + FileSystemRuntimeConfigLoader loader = new(fs); + + // Act + bool loaded = loader.TryLoadConfig("dab-config.json", out RuntimeConfig runtimeConfig); + + // Assert + Assert.IsTrue(loaded, "Config should load successfully."); + Assert.IsTrue(runtimeConfig.EnableAggregation, + "EnableAggregation should be true when explicitly set to true in config."); + } } diff --git a/src/Service.Tests/GraphQLBuilder/QueryBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/QueryBuilderTests.cs index c257a86054..9e7feb303f 100644 --- a/src/Service.Tests/GraphQLBuilder/QueryBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/QueryBuilderTests.cs @@ -538,6 +538,119 @@ public void GenerateReturnType_IncludesGroupByField() Assert.AreEqual("BookGroupBy", groupByType.Name.Value, "should return GroupBy type"); } + /// + /// Tests that the return type does NOT include the groupBy field when aggregation is disabled. + /// + [TestMethod] + [TestCategory("Query Builder - Return Type")] + public void GenerateReturnType_ExcludesGroupByField_WhenAggregationDisabled() + { + // Arrange + NameNode entityName = new("Book"); + + // Act + ObjectTypeDefinitionNode returnType = QueryBuilder.GenerateReturnType(entityName, isAggregationEnabled: false); + + // Assert + FieldDefinitionNode groupByField = returnType.Fields.FirstOrDefault(f => f.Name.Value == "groupBy"); + Assert.IsNull(groupByField, "groupBy field should NOT exist when aggregation is disabled"); + } + + /// + /// Tests that QueryBuilder.Build adds a groupBy field to the connection type + /// for MSSQL entities when aggregation is enabled. This verifies the fix for the + /// regression where aggregation features were not accessible in the schema. + /// + [TestMethod] + [TestCategory("Query Builder - Aggregation")] + public void Build_WithMssqlAndAggregationEnabled_AddsGroupByToConnectionType() + { + // Arrange + string gql = @" +type Book @model(name:""Book"") { + id: ID! + price: Float! + title: String +}"; + + DocumentNode root = Utf8GraphQLParser.Parse(gql); + Dictionary entityNameToDatabaseType = new() + { + { "Book", DatabaseType.MSSQL } + }; + + Dictionary bookPermissions = GraphQLTestHelpers.CreateStubEntityPermissionsMap( + new string[] { "Book" }, + new EntityActionOperation[] { EntityActionOperation.Read }, + new string[] { "anonymous" }); + + // Act + DocumentNode queryRoot = QueryBuilder.Build( + root, + entityNameToDatabaseType, + new(new Dictionary { { "Book", GraphQLTestHelpers.GenerateEmptyEntity() } }), + inputTypes: new(), + entityPermissionsMap: bookPermissions, + _isAggregationEnabled: true + ); + + // Assert: find BookConnection type + ObjectTypeDefinitionNode bookConnection = queryRoot.Definitions + .OfType() + .FirstOrDefault(d => d.Name.Value == "BookConnection"); + Assert.IsNotNull(bookConnection, "BookConnection type should exist"); + + FieldDefinitionNode groupByField = bookConnection.Fields.FirstOrDefault(f => f.Name.Value == "groupBy"); + Assert.IsNotNull(groupByField, "groupBy field should exist on BookConnection when aggregation is enabled for MSSQL"); + } + + /// + /// Tests that QueryBuilder.Build does NOT add a groupBy field to the connection type + /// for PostgreSQL entities, since aggregation is only enabled for MSSQL and DWSQL. + /// + [TestMethod] + [TestCategory("Query Builder - Aggregation")] + public void Build_WithPostgreSqlAndAggregationEnabled_DoesNotAddGroupByToConnectionType() + { + // Arrange + string gql = @" +type Book @model(name:""Book"") { + id: ID! + price: Float! + title: String +}"; + + DocumentNode root = Utf8GraphQLParser.Parse(gql); + Dictionary entityNameToDatabaseType = new() + { + { "Book", DatabaseType.PostgreSQL } + }; + + Dictionary bookPermissions = GraphQLTestHelpers.CreateStubEntityPermissionsMap( + new string[] { "Book" }, + new EntityActionOperation[] { EntityActionOperation.Read }, + new string[] { "anonymous" }); + + // Act + DocumentNode queryRoot = QueryBuilder.Build( + root, + entityNameToDatabaseType, + new(new Dictionary { { "Book", GraphQLTestHelpers.GenerateEmptyEntity() } }), + inputTypes: new(), + entityPermissionsMap: bookPermissions, + _isAggregationEnabled: true + ); + + // Assert: find BookConnection type + ObjectTypeDefinitionNode bookConnection = queryRoot.Definitions + .OfType() + .FirstOrDefault(d => d.Name.Value == "BookConnection"); + Assert.IsNotNull(bookConnection, "BookConnection type should exist"); + + FieldDefinitionNode groupByField = bookConnection.Fields.FirstOrDefault(f => f.Name.Value == "groupBy"); + Assert.IsNull(groupByField, "groupBy field should NOT exist on BookConnection for PostgreSQL (not in AggregationEnabledDatabaseTypes)"); + } + public static ObjectTypeDefinitionNode GetQueryNode(DocumentNode queryRoot) { return (ObjectTypeDefinitionNode)queryRoot.Definitions.First(d => d is ObjectTypeDefinitionNode node && node.Name.Value == "Query"); From 5fcfec048971e39eb79d2f21ca6133c4386648cb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 15 Apr 2026 22:21:35 +0000 Subject: [PATCH 3/3] Refactor: extract shared GQL constant and permissions helper in QueryBuilderTests Agent-Logs-Url: https://github.com/Azure/data-api-builder/sessions/b4c70fb5-8795-4c8a-a904-d54344395c93 Co-authored-by: Aniruddh25 <3513779+Aniruddh25@users.noreply.github.com> --- .../GraphQLBuilder/QueryBuilderTests.cs | 50 ++++++++----------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/src/Service.Tests/GraphQLBuilder/QueryBuilderTests.cs b/src/Service.Tests/GraphQLBuilder/QueryBuilderTests.cs index 9e7feb303f..56cac93c8a 100644 --- a/src/Service.Tests/GraphQLBuilder/QueryBuilderTests.cs +++ b/src/Service.Tests/GraphQLBuilder/QueryBuilderTests.cs @@ -18,6 +18,16 @@ public class QueryBuilderTests { private const int NUMBER_OF_ARGUMENTS = 4; + /// + /// GQL schema for a Book entity with numeric fields, used for aggregation tests. + /// + private const string BOOK_WITH_NUMERIC_FIELDS_GQL = @" +type Book @model(name:""Book"") { + id: ID! + price: Float! + title: String +}"; + private Dictionary _entityPermissions; /// @@ -37,6 +47,14 @@ public void SetupEntityPermissionsMap() ); } + private static Dictionary CreateBookEntityPermissions() + { + return GraphQLTestHelpers.CreateStubEntityPermissionsMap( + new string[] { "Book" }, + new EntityActionOperation[] { EntityActionOperation.Read }, + new string[] { "anonymous" }); + } + [DataTestMethod] [TestCategory("Query Generation")] [TestCategory("Single item access")] @@ -566,31 +584,19 @@ public void GenerateReturnType_ExcludesGroupByField_WhenAggregationDisabled() public void Build_WithMssqlAndAggregationEnabled_AddsGroupByToConnectionType() { // Arrange - string gql = @" -type Book @model(name:""Book"") { - id: ID! - price: Float! - title: String -}"; - - DocumentNode root = Utf8GraphQLParser.Parse(gql); + DocumentNode root = Utf8GraphQLParser.Parse(BOOK_WITH_NUMERIC_FIELDS_GQL); Dictionary entityNameToDatabaseType = new() { { "Book", DatabaseType.MSSQL } }; - Dictionary bookPermissions = GraphQLTestHelpers.CreateStubEntityPermissionsMap( - new string[] { "Book" }, - new EntityActionOperation[] { EntityActionOperation.Read }, - new string[] { "anonymous" }); - // Act DocumentNode queryRoot = QueryBuilder.Build( root, entityNameToDatabaseType, new(new Dictionary { { "Book", GraphQLTestHelpers.GenerateEmptyEntity() } }), inputTypes: new(), - entityPermissionsMap: bookPermissions, + entityPermissionsMap: CreateBookEntityPermissions(), _isAggregationEnabled: true ); @@ -613,31 +619,19 @@ type Book @model(name:""Book"") { public void Build_WithPostgreSqlAndAggregationEnabled_DoesNotAddGroupByToConnectionType() { // Arrange - string gql = @" -type Book @model(name:""Book"") { - id: ID! - price: Float! - title: String -}"; - - DocumentNode root = Utf8GraphQLParser.Parse(gql); + DocumentNode root = Utf8GraphQLParser.Parse(BOOK_WITH_NUMERIC_FIELDS_GQL); Dictionary entityNameToDatabaseType = new() { { "Book", DatabaseType.PostgreSQL } }; - Dictionary bookPermissions = GraphQLTestHelpers.CreateStubEntityPermissionsMap( - new string[] { "Book" }, - new EntityActionOperation[] { EntityActionOperation.Read }, - new string[] { "anonymous" }); - // Act DocumentNode queryRoot = QueryBuilder.Build( root, entityNameToDatabaseType, new(new Dictionary { { "Book", GraphQLTestHelpers.GenerateEmptyEntity() } }), inputTypes: new(), - entityPermissionsMap: bookPermissions, + entityPermissionsMap: CreateBookEntityPermissions(), _isAggregationEnabled: true );