Skip to content

Commit 2240172

Browse files
committed
2 parents db72d87 + 6c54b84 commit 2240172

5 files changed

Lines changed: 121 additions & 6 deletions

File tree

CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# These owners will be the default owners for everything in
22
# the repo. Unless a later match takes precedence,
33
# review when someone opens a pull request.
4-
* @Aniruddh25 @aaronburtle @anushakolan @RubenCerna2079 @souvikghosh04 @ravishetye @neeraj-sharma2592 @sourabh1007 @vadeveka @Alekhya-Polavarapu @rusamant
4+
* @Aniruddh25 @aaronburtle @anushakolan @RubenCerna2079 @souvikghosh04 @akashkumar58 @neeraj-sharma2592 @sourabh1007 @vadeveka @Alekhya-Polavarapu @rusamant
55

66
code_of_conduct.md @jerrynixon
77
contributing.md @jerrynixon

src/Core/Services/MetadataProviders/SqlMetadataProvider.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,6 +1151,19 @@ private async Task PopulateResultSetDefinitionsForStoredProcedureAsync(
11511151
Type resultFieldType = SqlToCLRType(element.GetProperty(BaseSqlQueryBuilder.STOREDPROC_COLUMN_SYSTEMTYPENAME).ToString());
11521152
bool isResultFieldNullable = element.GetProperty(BaseSqlQueryBuilder.STOREDPROC_COLUMN_ISNULLABLE).GetBoolean();
11531153

1154+
// Validate that the stored procedure returns columns with proper names
1155+
// This commonly occurs when using aggregate functions or expressions without aliases
1156+
if (string.IsNullOrWhiteSpace(resultFieldName))
1157+
{
1158+
throw new DataApiBuilderException(
1159+
message: $"The stored procedure '{dbStoredProcedureName}' returns a column without a name. " +
1160+
"This typically happens when using aggregate functions (like MAX, MIN, COUNT) or expressions " +
1161+
"without providing an alias. Please add column aliases to your SELECT statement. " +
1162+
"For example: 'SELECT MAX(id) AS MaxId' instead of 'SELECT MAX(id)'.",
1163+
statusCode: HttpStatusCode.ServiceUnavailable,
1164+
subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization);
1165+
}
1166+
11541167
// Store the dictionary containing result set field with its type as Columns
11551168
storedProcedureDefinition.Columns.TryAdd(resultFieldName, new(resultFieldType) { IsNullable = isResultFieldNullable });
11561169
}

src/Service.Tests/Configuration/Telemetry/AzureLogAnalyticsTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ public async Task TestAzureLogAnalyticsFlushServiceSucceed(string message, LogLe
120120

121121
_ = Task.Run(() => flusherService.StartAsync(tokenSource.Token));
122122

123-
await Task.Delay(1000);
123+
await Task.Delay(2000);
124124

125125
// Assert
126126
AzureLogAnalyticsLogs actualLog = customClient.LogAnalyticsLogs[0];

src/Service.Tests/UnitTests/SqlMetadataProviderUnitTests.cs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,23 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Data.Common;
67
using System.IO;
78
using System.Net;
9+
using System.Text.Json.Nodes;
810
using System.Threading.Tasks;
911
using Azure.DataApiBuilder.Config.DatabasePrimitives;
1012
using Azure.DataApiBuilder.Config.ObjectModel;
1113
using Azure.DataApiBuilder.Core.Authorization;
1214
using Azure.DataApiBuilder.Core.Configurations;
15+
using Azure.DataApiBuilder.Core.Models;
1316
using Azure.DataApiBuilder.Core.Resolvers;
1417
using Azure.DataApiBuilder.Core.Resolvers.Factories;
1518
using Azure.DataApiBuilder.Core.Services;
1619
using Azure.DataApiBuilder.Service.Exceptions;
1720
using Azure.DataApiBuilder.Service.Tests.Configuration;
1821
using Azure.DataApiBuilder.Service.Tests.SqlTests;
22+
using Microsoft.AspNetCore.Http;
1923
using Microsoft.Data.SqlClient;
2024
using Microsoft.Extensions.Logging;
2125
using Microsoft.VisualStudio.TestTools.UnitTesting;
@@ -399,6 +403,100 @@ public async Task ValidateInferredRelationshipInfoForPgSql()
399403
ValidateInferredRelationshipInfoForTables();
400404
}
401405

406+
/// <summary>
407+
/// Data-driven test to validate that DataApiBuilderException is thrown for various invalid resultFieldName values
408+
/// during stored procedure result set definition population.
409+
/// </summary>
410+
[DataTestMethod, TestCategory(TestCategory.MSSQL)]
411+
[DataRow(null, DisplayName = "Null result field name")]
412+
[DataRow("", DisplayName = "Empty result field name")]
413+
[DataRow(" ", DisplayName = "Multiple spaces result field name")]
414+
public async Task ValidateExceptionForInvalidResultFieldNames(string invalidFieldName)
415+
{
416+
DatabaseEngine = TestCategory.MSSQL;
417+
TestHelper.SetupDatabaseEnvironment(DatabaseEngine);
418+
RuntimeConfig baseConfigFromDisk = SqlTestHelper.SetupRuntimeConfig();
419+
420+
// Create a RuntimeEntities with ONLY our test stored procedure entity
421+
Dictionary<string, Entity> entitiesDictionary = new()
422+
{
423+
{
424+
"get_book_by_id", new Entity(
425+
Source: new("dbo.get_book_by_id", EntitySourceType.StoredProcedure, null, null),
426+
Rest: new(Enabled: true),
427+
GraphQL: new("get_book_by_id", "get_book_by_ids", Enabled: true),
428+
Permissions: new EntityPermission[] {
429+
new(
430+
Role: "anonymous",
431+
Actions: new EntityAction[] {
432+
new(Action: EntityActionOperation.Execute, Fields: null, Policy: null)
433+
})
434+
},
435+
Relationships: null,
436+
Mappings: null
437+
)
438+
}
439+
};
440+
441+
RuntimeEntities entities = new(entitiesDictionary);
442+
RuntimeConfig runtimeConfig = baseConfigFromDisk with { Entities = entities };
443+
RuntimeConfigProvider runtimeConfigProvider = TestHelper.GenerateInMemoryRuntimeConfigProvider(runtimeConfig);
444+
ILogger<ISqlMetadataProvider> sqlMetadataLogger = new Mock<ILogger<ISqlMetadataProvider>>().Object;
445+
446+
// Setup query builder
447+
_queryBuilder = new MsSqlQueryBuilder();
448+
449+
try
450+
{
451+
string dataSourceName = runtimeConfigProvider.GetConfig().DefaultDataSourceName;
452+
453+
// Create mock query executor that always returns JsonArray with invalid field name
454+
Mock<IQueryExecutor> mockQueryExecutor = new();
455+
456+
// Create a JsonArray that simulates the stored procedure result with invalid field name
457+
JsonArray invalidFieldJsonArray = new();
458+
JsonObject jsonObject = new()
459+
{
460+
[BaseSqlQueryBuilder.STOREDPROC_COLUMN_NAME] = invalidFieldName, // This will be null, empty, or whitespace
461+
[BaseSqlQueryBuilder.STOREDPROC_COLUMN_SYSTEMTYPENAME] = "varchar",
462+
[BaseSqlQueryBuilder.STOREDPROC_COLUMN_ISNULLABLE] = false
463+
};
464+
invalidFieldJsonArray.Add(jsonObject);
465+
466+
// Setup the mock to return our malformed JsonArray for all ExecuteQueryAsync calls
467+
mockQueryExecutor.Setup(x => x.ExecuteQueryAsync(
468+
It.IsAny<string>(),
469+
It.IsAny<IDictionary<string, DbConnectionParam>>(),
470+
It.IsAny<Func<DbDataReader, List<string>, Task<JsonArray>>>(),
471+
It.IsAny<string>(),
472+
It.IsAny<HttpContext>(),
473+
It.IsAny<List<string>>()))
474+
.ReturnsAsync(invalidFieldJsonArray);
475+
476+
// Setup Mock query manager Factory
477+
Mock<IAbstractQueryManagerFactory> queryManagerFactory = new();
478+
queryManagerFactory.Setup(x => x.GetQueryBuilder(It.IsAny<DatabaseType>())).Returns(_queryBuilder);
479+
queryManagerFactory.Setup(x => x.GetQueryExecutor(It.IsAny<DatabaseType>())).Returns(mockQueryExecutor.Object);
480+
481+
ISqlMetadataProvider sqlMetadataProvider = new MsSqlMetadataProvider(
482+
runtimeConfigProvider,
483+
queryManagerFactory.Object,
484+
sqlMetadataLogger,
485+
dataSourceName);
486+
487+
await sqlMetadataProvider.InitializeAsync();
488+
Assert.Fail($"Expected DataApiBuilderException was not thrown for invalid resultFieldName: '{invalidFieldName}'.");
489+
}
490+
catch (DataApiBuilderException ex)
491+
{
492+
Assert.AreEqual(HttpStatusCode.ServiceUnavailable, ex.StatusCode);
493+
Assert.AreEqual(DataApiBuilderException.SubStatusCodes.ErrorInInitialization, ex.SubStatusCode);
494+
Assert.IsTrue(ex.Message.Contains("returns a column without a name"));
495+
}
496+
497+
TestHelper.UnsetAllDABEnvironmentVariables();
498+
}
499+
402500
/// <summary>
403501
/// Helper method for test methods ValidateInferredRelationshipInfoFor{MsSql, MySql, and PgSql}.
404502
/// This helper validates that an entity's relationship data is correctly inferred based on config and database supplied relationship metadata.

src/Service/Telemetry/AzureLogAnalyticsCustomLogCollector.cs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,18 @@ await _logs.Writer.WriteAsync(
5353
public async Task<List<AzureLogAnalyticsLogs>> DequeueAllAsync(string dabIdentifier, int flushIntervalSeconds)
5454
{
5555
List<AzureLogAnalyticsLogs> list = new();
56-
Stopwatch time = Stopwatch.StartNew();
5756

5857
if (await _logs.Reader.WaitToReadAsync())
5958
{
60-
while (_logs.Reader.TryRead(out AzureLogAnalyticsLogs? item))
59+
Stopwatch time = Stopwatch.StartNew();
60+
61+
while (true)
6162
{
62-
item.Identifier = dabIdentifier;
63-
list.Add(item);
63+
if (_logs.Reader.TryRead(out AzureLogAnalyticsLogs? item))
64+
{
65+
item.Identifier = dabIdentifier;
66+
list.Add(item);
67+
}
6468

6569
if (time.Elapsed >= TimeSpan.FromSeconds(flushIntervalSeconds))
6670
{

0 commit comments

Comments
 (0)