diff --git a/src/Service/Models/DbOperationResultRow.cs b/src/Service/Models/DbOperationResultRow.cs
new file mode 100644
index 0000000000..1e21aaea8c
--- /dev/null
+++ b/src/Service/Models/DbOperationResultRow.cs
@@ -0,0 +1,31 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Collections.Generic;
+
+namespace Azure.DataApiBuilder.Service.Models
+{
+ ///
+ /// Represents a single row read from DbDataReader.
+ ///
+ public class DbOperationResultRow
+ {
+ public DbOperationResultRow(
+ Dictionary columns,
+ Dictionary resultProperties)
+ {
+ this.Columns = columns;
+ this.ResultProperties = resultProperties;
+ }
+
+ ///
+ /// Represents a result set row in ColumnName: Value format, empty if no row was found.
+ ///
+ public Dictionary Columns { get; private set; }
+
+ ///
+ /// Represents DbDataReader properties such as RecordsAffected and HasRows.
+ ///
+ public Dictionary ResultProperties { get; private set; }
+ }
+}
diff --git a/src/Service/Resolvers/IQueryExecutor.cs b/src/Service/Resolvers/IQueryExecutor.cs
index b967ff5f84..2a9934b7f6 100644
--- a/src/Service/Resolvers/IQueryExecutor.cs
+++ b/src/Service/Resolvers/IQueryExecutor.cs
@@ -6,6 +6,7 @@
using System.Data.Common;
using System.Text.Json.Nodes;
using System.Threading.Tasks;
+using Azure.DataApiBuilder.Service.Models;
using Microsoft.AspNetCore.Http;
namespace Azure.DataApiBuilder.Service.Resolvers
@@ -30,7 +31,7 @@ public interface IQueryExecutor
public Task ExecuteQueryAsync(
string sqltext,
IDictionary parameters,
- Func?, Task>? dataReaderHandler,
+ Func?, Task>? dataReaderHandler,
HttpContext? httpContext = null,
List? args = null);
@@ -41,7 +42,7 @@ public interface IQueryExecutor
/// A DbDataReader.
/// List of string arguments if any.
/// A JsonArray with each element corresponding to the row (ColumnName : columnValue) in the dbDataReader.
- public Task GetJsonArrayAsync(
+ public Task GetJsonArrayAsync(
DbDataReader dbDataReader,
List? args = null);
@@ -62,10 +63,8 @@ public interface IQueryExecutor
///
/// A DbDataReader
/// List of columns to extract. Extracts all if unspecified.
- /// A tuple of 2 dictionaries:
- /// 1. A dictionary representing the row in ColumnName: Value format, null if no row was found
- /// 2. A dictionary of properties of the Db Data Reader like RecordsAffected, HasRows.
- public Task?, Dictionary>?> ExtractRowFromDbDataReader(
+ /// Single row read from DbDataReader.
+ public Task ExtractRowFromDbDataReader(
DbDataReader dbDataReader,
List? args = null);
@@ -76,11 +75,10 @@ public interface IQueryExecutor
///
/// A DbDataReader.
/// The arguments to this handler - args[0] = primary key in pretty format, args[1] = entity name.
- /// A tuple of 2 dictionaries:
- /// 1. A dictionary representing the row in ColumnName: Value format.
- /// 2. A dictionary of properties of the DbDataReader like RecordsAffected, HasRows.
- /// If the first result set is being returned, has the property "IsFirstResultSet" set to true in this dictionary.
- public Task?, Dictionary>?> GetMultipleResultSetsIfAnyAsync(
+ /// Single row read from DbDataReader.
+ /// If the first result set is being returned, DbOperationResultRow.ResultProperties dictionary has
+ /// the property "IsFirstResultSet" set to true.
+ public Task GetMultipleResultSetsIfAnyAsync(
DbDataReader dbDataReader,
List? args = null);
@@ -90,7 +88,7 @@ public interface IQueryExecutor
/// A DbDataReader.
/// List of string arguments if any.
/// A dictionary of properties of the DbDataReader like RecordsAffected, HasRows.
- public Task?> GetResultProperties(
+ public Task> GetResultProperties(
DbDataReader dbDataReader,
List? args = null);
diff --git a/src/Service/Resolvers/QueryExecutor.cs b/src/Service/Resolvers/QueryExecutor.cs
index 8e12c733bf..24938bbe3a 100644
--- a/src/Service/Resolvers/QueryExecutor.cs
+++ b/src/Service/Resolvers/QueryExecutor.cs
@@ -12,6 +12,7 @@
using System.Threading.Tasks;
using Azure.DataApiBuilder.Service.Configurations;
using Azure.DataApiBuilder.Service.Exceptions;
+using Azure.DataApiBuilder.Service.Models;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Polly;
@@ -62,7 +63,7 @@ public QueryExecutor(DbExceptionParser dbExceptionParser,
public virtual async Task ExecuteQueryAsync(
string sqltext,
IDictionary parameters,
- Func?, Task>? dataReaderHandler,
+ Func?, Task>? dataReaderHandler,
HttpContext? httpContext = null,
List? args = null)
{
@@ -134,7 +135,7 @@ await ExecuteQueryAgainstDbAsync(conn,
TConnection conn,
string sqltext,
IDictionary parameters,
- Func?, Task>? dataReaderHandler,
+ Func?, Task>? dataReaderHandler,
HttpContext? httpContext,
List? args = null)
{
@@ -208,12 +209,12 @@ public async Task ReadAsync(DbDataReader reader)
}
///
- public async Task?, Dictionary>?>
+ public async Task
ExtractRowFromDbDataReader(DbDataReader dbDataReader, List? args = null)
{
- Dictionary row = new();
-
- Dictionary propertiesOfResult = GetResultProperties(dbDataReader).Result ?? new();
+ DbOperationResultRow dbOperationResultRow = new(
+ columns: new(),
+ resultProperties: GetResultProperties(dbDataReader).Result ?? new());
if (await ReadAsync(dbDataReader))
{
@@ -221,7 +222,7 @@ public async Task ReadAsync(DbDataReader reader)
{
DataTable? schemaTable = dbDataReader.GetSchemaTable();
- if (schemaTable != null)
+ if (schemaTable is not null)
{
foreach (DataRow schemaRow in schemaTable.Rows)
{
@@ -235,43 +236,37 @@ public async Task ReadAsync(DbDataReader reader)
int colIndex = dbDataReader.GetOrdinal(columnName);
if (!dbDataReader.IsDBNull(colIndex))
{
- row.Add(columnName, dbDataReader[columnName]);
+ dbOperationResultRow.Columns.Add(columnName, dbDataReader[columnName]);
}
else
{
- row.Add(columnName, value: null);
+ dbOperationResultRow.Columns.Add(columnName, value: null);
}
}
}
}
}
- // no row was read
- if (row.Count == 0)
- {
- return new Tuple?, Dictionary>(null, propertiesOfResult);
- }
-
- return new Tuple?, Dictionary>(row, propertiesOfResult);
+ return dbOperationResultRow;
}
///
/// This function is a DbDataReader handler of type Func?, Task>
/// The parameter args is not used but is added to conform to the signature of the DbDataReader handler
/// function argument of ExecuteQueryAsync.
- public async Task GetJsonArrayAsync(
+ public async Task GetJsonArrayAsync(
DbDataReader dbDataReader,
List? args = null)
{
- Tuple?, Dictionary>? resultRowAndProperties;
+ DbOperationResultRow dbOperationResultRow = await ExtractRowFromDbDataReader(dbDataReader);
JsonArray resultArray = new();
- while ((resultRowAndProperties = await ExtractRowFromDbDataReader(dbDataReader)) is not null &&
- resultRowAndProperties.Item1 is not null)
+ while (dbOperationResultRow.Columns.Count > 0)
{
JsonElement result =
- JsonSerializer.Deserialize(JsonSerializer.Serialize(resultRowAndProperties.Item1));
+ JsonSerializer.Deserialize(JsonSerializer.Serialize(dbOperationResultRow.Columns));
resultArray.Add(result);
+ dbOperationResultRow = await ExtractRowFromDbDataReader(dbDataReader);
}
return resultArray;
@@ -306,21 +301,20 @@ public async Task ReadAsync(DbDataReader reader)
///
/// This function is a DbDataReader handler of type
/// Func?, Task>
- public async Task?, Dictionary>?> GetMultipleResultSetsIfAnyAsync(
+ public async Task GetMultipleResultSetsIfAnyAsync(
DbDataReader dbDataReader, List? args = null)
{
- Tuple?, Dictionary>? resultRecordWithProperties
+ DbOperationResultRow dbOperationResultRow
= await ExtractRowFromDbDataReader(dbDataReader);
/// Processes a second result set from DbDataReader if it exists.
/// In MsSQL upsert:
/// result set #1: result of the UPDATE operation.
/// result set #2: result of the INSERT operation.
- if (resultRecordWithProperties is not null && resultRecordWithProperties.Item1 is not null)
+ if (dbOperationResultRow.Columns.Count > 0)
{
- resultRecordWithProperties.Item2.Add(SqlMutationEngine.IS_FIRST_RESULT_SET, true);
- return new Tuple?, Dictionary>
- (resultRecordWithProperties.Item1, resultRecordWithProperties.Item2);
+ dbOperationResultRow.ResultProperties.Add(SqlMutationEngine.IS_FIRST_RESULT_SET, true);
+ return dbOperationResultRow;
}
else if (await dbDataReader.NextResultAsync())
{
@@ -345,20 +339,22 @@ public async Task ReadAsync(DbDataReader reader)
}
}
- return null;
+ return dbOperationResultRow;
}
///
/// This function is a DbDataReader handler of type
/// Func?, Task>
- public Task?> GetResultProperties(
+ public Task> GetResultProperties(
DbDataReader dbDataReader,
List? columnNames = null)
{
- Dictionary? propertiesOfResult = new();
- propertiesOfResult.Add(nameof(dbDataReader.RecordsAffected), dbDataReader.RecordsAffected);
- propertiesOfResult.Add(nameof(dbDataReader.HasRows), dbDataReader.HasRows);
- return Task.FromResult((Dictionary?)propertiesOfResult);
+ Dictionary resultProperties = new()
+ {
+ { nameof(dbDataReader.RecordsAffected), dbDataReader.RecordsAffected },
+ { nameof(dbDataReader.HasRows), dbDataReader.HasRows }
+ };
+ return Task.FromResult(resultProperties);
}
private async Task GetJsonStringFromDbReader(DbDataReader dbDataReader)
diff --git a/src/Service/Resolvers/SqlMutationEngine.cs b/src/Service/Resolvers/SqlMutationEngine.cs
index fd7bee2843..48c98e0ef6 100644
--- a/src/Service/Resolvers/SqlMutationEngine.cs
+++ b/src/Service/Resolvers/SqlMutationEngine.cs
@@ -117,14 +117,14 @@ await PerformDeleteOperation(
}
else
{
- Tuple?, Dictionary>? resultRowAndProperties =
+ DbOperationResultRow? mutationResult =
await PerformMutationOperation(
entityName,
mutationOperation,
parameters,
context);
- if (resultRowAndProperties is not null && resultRowAndProperties.Item1 is not null
+ if (mutationResult is not null && mutationResult.Columns.Count > 0
&& !context.Selection.Type.IsScalarType())
{
// Because the GraphQL mutation result set columns were exposed (mapped) column names,
@@ -133,7 +133,7 @@ await PerformMutationOperation(
// represent database column names.
result = await _queryEngine.ExecuteAsync(
context,
- GetBackingColumnsFromCollection(entityName, resultRowAndProperties.Item1));
+ GetBackingColumnsFromCollection(entityName, mutationResult.Columns));
}
}
@@ -287,18 +287,17 @@ await PerformDeleteOperation(
}
else if (context.OperationType is Config.Operation.Upsert || context.OperationType is Config.Operation.UpsertIncremental)
{
- Tuple?, Dictionary>? resultRowAndProperties =
+ DbOperationResultRow? upsertOperationResult =
await PerformUpsertOperation(
parameters,
context);
- if (resultRowAndProperties is not null &&
- resultRowAndProperties.Item1 is not null)
+ if (upsertOperationResult is not null && upsertOperationResult.Columns.Count > 0)
{
- Dictionary resultRow = resultRowAndProperties.Item1;
+ Dictionary resultRow = upsertOperationResult.Columns;
bool isFirstResultSet = false;
- if (resultRowAndProperties.Item2.TryGetValue(IS_FIRST_RESULT_SET, out object? isFirstResultSetValue))
+ if (upsertOperationResult.ResultProperties.TryGetValue(IS_FIRST_RESULT_SET, out object? isFirstResultSetValue))
{
isFirstResultSet = Convert.ToBoolean(isFirstResultSetValue);
}
@@ -320,7 +319,7 @@ await PerformUpsertOperation(
}
else
{
- Tuple?, Dictionary>? resultRowAndProperties =
+ DbOperationResultRow? mutationResult =
await PerformMutationOperation(
context.EntityName,
context.OperationType,
@@ -328,25 +327,22 @@ await PerformMutationOperation(
if (context.OperationType is Config.Operation.Insert)
{
- if (resultRowAndProperties is null || resultRowAndProperties.Item1 is null)
+ if (mutationResult is null || mutationResult.Columns.Count == 0)
{
// this case should not happen, we throw an exception
// which will be returned as an Unexpected Internal Server Error
throw new Exception();
}
- Dictionary resultRow = resultRowAndProperties.Item1;
- string primaryKeyRoute = ConstructPrimaryKeyRoute(context, resultRow);
+ string primaryKeyRoute = ConstructPrimaryKeyRoute(context, mutationResult.Columns);
// location will be updated in rest controller where httpcontext is available
- return new CreatedResult(location: primaryKeyRoute, OkMutationResponse(resultRow).Value);
+ return new CreatedResult(location: primaryKeyRoute, OkMutationResponse(mutationResult.Columns).Value);
}
if (context.OperationType is Config.Operation.Update || context.OperationType is Config.Operation.UpdateIncremental)
{
// Nothing to update means we throw Exception
- if (resultRowAndProperties is null ||
- resultRowAndProperties.Item1 is null ||
- resultRowAndProperties.Item1.Count == 0)
+ if (mutationResult is null || mutationResult.Columns.Count == 0)
{
throw new DataApiBuilderException(message: "No Update could be performed, record not found",
statusCode: HttpStatusCode.PreconditionFailed,
@@ -354,7 +350,7 @@ resultRowAndProperties.Item1 is null ||
}
// Valid REST updates return OkObjectResult
- return OkMutationResponse(resultRowAndProperties.Item1);
+ return OkMutationResponse(mutationResult.Columns);
}
}
@@ -413,10 +409,8 @@ private static OkObjectResult OkMutationResponse(JsonElement jsonResult)
/// This cannot be Delete, Upsert or UpsertIncremental since those operations have dedicated functions.
/// The parameters of the mutation query.
/// In the case of GraphQL, the HotChocolate library's middleware context.
- /// A tuple of 2 dictionaries:
- /// 1. A dictionary representing the row in ColumnName: Value format, null if no row is mutated.
- /// 2. A dictionary of properties of the Db Data Reader like RecordsAffected, HasRows.
- private async Task?, Dictionary>?>
+ /// Single row read from DbDataReader.
+ private async Task
PerformMutationOperation(
string entityName,
Config.Operation operationType,
@@ -493,7 +487,7 @@ private static OkObjectResult OkMutationResponse(JsonElement jsonResult)
throw new NotSupportedException($"Unexpected mutation operation \" {operationType}\" requested.");
}
- Tuple?, Dictionary>? resultRecord = null;
+ DbOperationResultRow? dbOperationResultRow;
if (context is not null && !context.Selection.Type.IsScalarType())
{
@@ -517,7 +511,7 @@ private static OkObjectResult OkMutationResponse(JsonElement jsonResult)
// which would fail to get the mutated entry from the db
// When no exposed column names were resolved, it is safe to provide
// backing column names (sourceDefinition.Primary) as a list of arguments.
- resultRecord =
+ dbOperationResultRow =
await _queryExecutor.ExecuteQueryAsync(
queryString,
queryParameters,
@@ -525,7 +519,7 @@ await _queryExecutor.ExecuteQueryAsync(
_httpContextAccessor.HttpContext!,
primaryKeyExposedColumnNames.Count > 0 ? primaryKeyExposedColumnNames : sourceDefinition.PrimaryKey);
- if (resultRecord is not null && resultRecord.Item1 is null)
+ if (dbOperationResultRow is not null && dbOperationResultRow.Columns.Count == 0)
{
string searchedPK;
if (primaryKeyExposedColumnNames.Count > 0)
@@ -547,7 +541,7 @@ await _queryExecutor.ExecuteQueryAsync(
{
// This is the scenario for all REST mutation operations covered by this function
// and the case when the Selection Type is a scalar for GraphQL.
- resultRecord =
+ dbOperationResultRow =
await _queryExecutor.ExecuteQueryAsync(
queryString,
queryParameters,
@@ -555,7 +549,7 @@ await _queryExecutor.ExecuteQueryAsync(
_httpContextAccessor.HttpContext!);
}
- return resultRecord;
+ return dbOperationResultRow;
}
///
@@ -605,10 +599,8 @@ private async Task?>
///
/// The parameters for the mutation query.
/// The REST request context.
- /// A tuple of 2 dictionaries:
- /// 1. A dictionary representing the row in ColumnName: Value format, null if no row was found
- /// 2. A dictionary of properties of the Db Data Reader like RecordsAffected, HasRows.
- private async Task?, Dictionary>?>
+ /// Single row read from DbDataReader.
+ private async Task
PerformUpsertOperation(
IDictionary parameters,
RestRequestContext context)
diff --git a/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs b/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs
index dc0d0f636c..f6ea00e193 100644
--- a/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs
+++ b/src/Service/Services/MetadataProviders/SqlMetadataProvider.cs
@@ -15,6 +15,7 @@
using Azure.DataApiBuilder.Config;
using Azure.DataApiBuilder.Service.Configurations;
using Azure.DataApiBuilder.Service.Exceptions;
+using Azure.DataApiBuilder.Service.Models;
using Azure.DataApiBuilder.Service.Parsers;
using Azure.DataApiBuilder.Service.Resolvers;
using Microsoft.Extensions.Logging;
@@ -1360,17 +1361,17 @@ private static void ValidateAllFkHaveBeenInferred(
private async Task?>
SummarizeFkMetadata(DbDataReader reader, List? args = null)
{
- // Gets a tuple of 2 dictionaries:
- // 1. the first row extracted from the result
- // 2. Dictionary of the DbDataReader properties like RecordsAffected, HasRows.
- // This function only requires the result row i.e. Item1 from the tuple.
- Tuple?, Dictionary>? foreignKeyInfoWithProperties =
+ // Gets a single row read from DbDataReader which contains 2 dictionaries:
+ // 1. The columns of the first row extracted from the result
+ // 2. DbDataReader properties like RecordsAffected, HasRows.
+ // This function only requires the DbOperationResultRow.Columns property.
+ DbOperationResultRow foreignKeyInfoWithProperties =
await QueryExecutor.ExtractRowFromDbDataReader(reader);
Dictionary pairToFkDefinition = new();
- while (foreignKeyInfoWithProperties is not null && foreignKeyInfoWithProperties.Item1 is not null)
+ while (foreignKeyInfoWithProperties.Columns.Count > 0)
{
- Dictionary foreignKeyInfo = foreignKeyInfoWithProperties.Item1;
+ Dictionary foreignKeyInfo = foreignKeyInfoWithProperties.Columns;
string referencingSchemaName =
(string)foreignKeyInfo[$"Referencing{nameof(DatabaseObject.SchemaName)}"]!;
string referencingTableName = (string)foreignKeyInfo[$"Referencing{nameof(SourceDefinition)}"]!;