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