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
);