Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
155 changes: 155 additions & 0 deletions src/Service.Tests/Configuration/RuntimeConfigLoaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -283,4 +283,159 @@ public async Task ChildConfigLoadFailureHaltsParentConfigLoading()
}
}
}

/// <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"": {}
}";

IFileSystem fs = new MockFileSystem(new Dictionary<string, MockFileData>()
{
{ "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.");
}

/// <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"": {}
}";

IFileSystem fs = new MockFileSystem(new Dictionary<string, MockFileData>()
{
{ "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.");
}

/// <summary>
/// Tests that EnableAggregation returns false when explicitly disabled in config.
/// </summary>
[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<string, MockFileData>()
{
{ "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.");
}

/// <summary>
/// Tests that EnableAggregation returns true when explicitly enabled in config.
/// </summary>
[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<string, MockFileData>()
{
{ "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.");
}
}
107 changes: 107 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,95 @@ 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 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.
/// </summary>
[TestMethod]
[TestCategory("Query Builder - Aggregation")]
public void Build_WithMssqlAndAggregationEnabled_AddsGroupByToConnectionType()
{
// Arrange
DocumentNode root = Utf8GraphQLParser.Parse(BOOK_WITH_NUMERIC_FIELDS_GQL);
Dictionary<string, DatabaseType> entityNameToDatabaseType = new()
{
{ "Book", DatabaseType.MSSQL }
};

// 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");
Assert.IsNotNull(groupByField, "groupBy field should exist on BookConnection when aggregation is enabled for MSSQL");
}

/// <summary>
/// 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.
/// </summary>
[TestMethod]
[TestCategory("Query Builder - Aggregation")]
public void Build_WithPostgreSqlAndAggregationEnabled_DoesNotAddGroupByToConnectionType()
{
// Arrange
DocumentNode root = Utf8GraphQLParser.Parse(BOOK_WITH_NUMERIC_FIELDS_GQL);
Dictionary<string, DatabaseType> entityNameToDatabaseType = new()
{
{ "Book", DatabaseType.PostgreSQL }
};

// 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");
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");
Expand Down