Skip to content
Open
5 changes: 5 additions & 0 deletions schemas/dab.draft.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 5 additions & 3 deletions src/Config/ObjectModel/RuntimeConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,14 @@ Runtime.GraphQL is null ||
public string DefaultDataSourceName { get; set; }

/// <summary>
/// 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.
/// </summary>
[JsonIgnore]
public bool EnableAggregation =>
Runtime is not null &&
Runtime.GraphQL is not null &&
Runtime is null ||
Runtime.GraphQL is null ||
Runtime.GraphQL.EnableAggregation;

[JsonIgnore]
Expand Down
4 changes: 3 additions & 1 deletion src/Core/Services/GraphQLSchemaCreator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,14 @@ public GraphQLSchemaCreator(
/// <summary>
/// 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.
/// </summary>
protected void OnConfigChanged(object? sender, HotReloadEventArgs args)
{
RuntimeConfig runtimeConfig = _runtimeConfigProvider.GetConfig();
_isMultipleCreateOperationEnabled = runtimeConfig.IsMultipleCreateOperationEnabled();
_isAggregationEnabled = runtimeConfig.EnableAggregation;
_entities = runtimeConfig.Entities;
}

Expand Down
100 changes: 100 additions & 0 deletions src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -504,4 +504,104 @@ public async Task ChildConfigLoadFailureHaltsParentConfigLoading()
}
}
}

Comment thread
Aniruddh25 marked this conversation as resolved.
/// <summary>
/// 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.
/// </summary>
[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"": {}
}";

RuntimeConfig runtimeConfig = LoadConfig(configJson);

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.");
}

/// <summary>
/// Tests that EnableAggregation returns true by default when runtime section is absent.
/// </summary>
[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"": {}
}";

RuntimeConfig runtimeConfig = LoadConfig(configJson);

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.");
}

/// <summary>
/// Tests that EnableAggregation honours the explicit value set in the config file.
/// </summary>
[DataTestMethod]
[DataRow(true, DisplayName = "Explicit true is respected")]
[DataRow(false, DisplayName = "Explicit false is respected")]
public void EnableAggregation_WhenExplicitlySet_ReturnsConfiguredValue(bool explicitValue)
{
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"": {explicitValue.ToString().ToLower()}
}},
""host"": {{
""authentication"": {{ ""provider"": ""StaticWebApps"" }}
}}
}},
""entities"": {{}}
}}";

RuntimeConfig runtimeConfig = LoadConfig(configJson);

Assert.AreEqual(explicitValue, runtimeConfig.EnableAggregation,
$"EnableAggregation should be {explicitValue} when explicitly set to {explicitValue} in config.");
}

/// <summary>
/// Loads a <see cref="RuntimeConfig"/> from a JSON string using a mock file system.
/// </summary>
private static RuntimeConfig LoadConfig(string configJson)
{
IFileSystem fs = new MockFileSystem(new Dictionary<string, MockFileData>
{
{ "dab-config.json", new MockFileData(configJson) }
});

FileSystemRuntimeConfigLoader loader = new(fs);
Assert.IsTrue(loader.TryLoadConfig("dab-config.json", out RuntimeConfig config), "Config should load successfully.");
return config;
}
}
86 changes: 86 additions & 0 deletions src/Service.Tests/GraphQLBuilder/QueryBuilderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ public class QueryBuilderTests
{
private const int NUMBER_OF_ARGUMENTS = 4;

/// <summary>
/// GQL schema for a Book entity with numeric fields, used for aggregation tests.
/// </summary>
private const string BOOK_WITH_NUMERIC_FIELDS_GQL = @"
type Book @model(name:""Book"") {
id: ID!
price: Float!
title: String
}";

private Dictionary<string, EntityMetadata> _entityPermissions;

/// <summary>
Expand All @@ -37,6 +47,14 @@ public void SetupEntityPermissionsMap()
);
}

private static Dictionary<string, EntityMetadata> CreateBookEntityPermissions()
{
return GraphQLTestHelpers.CreateStubEntityPermissionsMap(
new string[] { "Book" },
new EntityActionOperation[] { EntityActionOperation.Read },
new string[] { "anonymous" });
}

[DataTestMethod]
[TestCategory("Query Generation")]
[TestCategory("Single item access")]
Expand Down Expand Up @@ -538,6 +556,74 @@ public void GenerateReturnType_IncludesGroupByField()
Assert.AreEqual("BookGroupBy", groupByType.Name.Value, "should return GroupBy type");
}

/// <summary>
/// Tests that the return type does NOT include the groupBy field when aggregation is disabled.
/// </summary>
[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");
}

/// <summary>
/// Tests that QueryBuilder.Build correctly adds or omits the groupBy field on the
/// connection type based on whether the database type is in
/// <see cref="QueryBuilder.AggregationEnabledDatabaseTypes"/>.
/// MSSQL and DWSQL are supported; other types (e.g. PostgreSQL) are not.
/// </summary>
[DataTestMethod]
[DataRow(DatabaseType.MSSQL, true, DisplayName = "MSSQL: groupBy field present when aggregation enabled")]
[DataRow(DatabaseType.DWSQL, true, DisplayName = "DWSQL: groupBy field present when aggregation enabled")]
[DataRow(DatabaseType.PostgreSQL, false, DisplayName = "PostgreSQL: groupBy field absent (not in AggregationEnabledDatabaseTypes)")]
[DataRow(DatabaseType.MySQL, false, DisplayName = "MySQL: groupBy field absent (not in AggregationEnabledDatabaseTypes)")]
[TestCategory("Query Builder - Aggregation")]
public void Build_WithAggregationEnabled_GroupByPresenceMatchesDatabaseSupport(
DatabaseType dbType,
bool expectGroupBy)
{
// Arrange
DocumentNode root = Utf8GraphQLParser.Parse(BOOK_WITH_NUMERIC_FIELDS_GQL);
Dictionary<string, DatabaseType> entityNameToDatabaseType = new()
{
{ "Book", dbType }
};

// Act
DocumentNode queryRoot = QueryBuilder.Build(
root,
entityNameToDatabaseType,
new(new Dictionary<string, Entity> { { "Book", GraphQLTestHelpers.GenerateEmptyEntity() } }),
inputTypes: new(),
entityPermissionsMap: CreateBookEntityPermissions(),
_isAggregationEnabled: true
);

// Assert: find BookConnection type
ObjectTypeDefinitionNode bookConnection = queryRoot.Definitions
.OfType<ObjectTypeDefinitionNode>()
.FirstOrDefault(d => d.Name.Value == "BookConnection");
Assert.IsNotNull(bookConnection, "BookConnection type should exist");

FieldDefinitionNode groupByField = bookConnection.Fields.FirstOrDefault(f => f.Name.Value == "groupBy");
if (expectGroupBy)
{
Assert.IsNotNull(groupByField, $"groupBy field should exist on BookConnection for {dbType}");
}
else
{
Assert.IsNull(groupByField, $"groupBy field should NOT exist on BookConnection for {dbType} (not in AggregationEnabledDatabaseTypes)");
}
}

public static ObjectTypeDefinitionNode GetQueryNode(DocumentNode queryRoot)
{
return (ObjectTypeDefinitionNode)queryRoot.Definitions.First(d => d is ObjectTypeDefinitionNode node && node.Name.Value == "Query");
Expand Down