diff --git a/doc/samples/SqlBatch_ExecuteReader.cs b/doc/samples/SqlBatch_ExecuteReader.cs new file mode 100644 index 0000000000..aadfbd3508 --- /dev/null +++ b/doc/samples/SqlBatch_ExecuteReader.cs @@ -0,0 +1,46 @@ +// +using Microsoft.Data.SqlClient; + +class Program +{ + static void Main() + { + string str = "Data Source=(local);Initial Catalog=Northwind;" + + "Integrated Security=SSPI;Encrypt=False"; + RunBatch(str); + } + + static void RunBatch(string connString) + { + using var connection = new SqlConnection(connString); + connection.Open(); + + var batch = new SqlBatch(connection); + + const int count = 10; + const string parameterName = "parameter"; + for (int i = 0; i < count; i++) + { + var batchCommand = new SqlBatchCommand($"SELECT @{parameterName} as value"); + batchCommand.Parameters.Add(new SqlParameter(parameterName, i)); + batch.BatchCommands.Add(batchCommand); + } + + // Optionally Prepare + batch.Prepare(); + + var results = new List(count); + using (SqlDataReader reader = batch.ExecuteReader()) + { + do + { + while (reader.Read()) + { + results.Add(reader.GetFieldValue(0)); + } + } while (reader.NextResult()); + } + Console.WriteLine(string.Join(", ", results)); + } +} +// diff --git a/doc/snippets/Microsoft.Data.Common/DbBatch.xml b/doc/snippets/Microsoft.Data.Common/DbBatch.xml new file mode 100644 index 0000000000..1c9dcdd68b --- /dev/null +++ b/doc/snippets/Microsoft.Data.Common/DbBatch.xml @@ -0,0 +1,651 @@ + + + + + + + + + System.Object + + + + System.IAsyncDisposable + + + System.IDisposable + + + + + [System.Runtime.CompilerServices.Nullable(0)] + [<System.Runtime.CompilerServices.Nullable(0)>] + + + [System.Runtime.CompilerServices.NullableContext(1)] + [<System.Runtime.CompilerServices.NullableContext(1)>] + + + + Represents a batch of commands which can be executed against a data source in a single round trip. Provides a base class for database-specific classes that represent command batches. + + + + + + + + + + + Constructor + + + + Initializes a new instance of the class. + + To be added. + + + + + + Property + + System.Data.Common.DbBatchCommandCollection + + + + Gets the collection of objects. + + The commands contained within the batch. + To be added. + + + + + + Method + + System.Void + + + + + Attempts to cancel the execution of a . + + + + + + + + + + + + Property + + + [System.Runtime.CompilerServices.Nullable(2)] + [<System.Runtime.CompilerServices.Nullable(2)>] + + + [get: System.Runtime.CompilerServices.NullableContext(2)] + [<get: System.Runtime.CompilerServices.NullableContext(2)>] + + + [set: System.Runtime.CompilerServices.NullableContext(2)] + [<set: System.Runtime.CompilerServices.NullableContext(2)>] + + + + System.Data.Common.DbConnection + + + + Gets or sets the used by this . + + The connection to the data source. + To be added. + + + + + + Method + + System.Data.Common.DbBatchCommand + + + + + Creates a new instance of a object. + + + A object. + + To be added. + + + + + + Method + + System.Data.Common.DbBatchCommand + + + + + When overridden in a derived class, creates a new instance of a object. + + + A object. + + To be added. + + + + + + Property + + System.Data.Common.DbBatchCommandCollection + + + + When overridden in a derived class, gets the collection of objects. + + The commands contained within the batch. + To be added. + + + + + + Property + + + [System.Runtime.CompilerServices.Nullable(2)] + [<System.Runtime.CompilerServices.Nullable(2)>] + + + [get: System.Runtime.CompilerServices.NullableContext(2)] + [<get: System.Runtime.CompilerServices.NullableContext(2)>] + + + [set: System.Runtime.CompilerServices.NullableContext(2)] + [<set: System.Runtime.CompilerServices.NullableContext(2)>] + + + + System.Data.Common.DbConnection + + + + When overridden in a derived class, gets or sets the used by this . + + The connection to the data source. + To be added. + + + + + + Property + + + [System.Runtime.CompilerServices.Nullable(2)] + [<System.Runtime.CompilerServices.Nullable(2)>] + + + [get: System.Runtime.CompilerServices.NullableContext(2)] + [<get: System.Runtime.CompilerServices.NullableContext(2)>] + + + [set: System.Runtime.CompilerServices.NullableContext(2)] + [<set: System.Runtime.CompilerServices.NullableContext(2)>] + + + + System.Data.Common.DbTransaction + + + + When overridden in a derived class, gets or sets the within which this object executes. + + + The transaction within which a batch of a .NET data provider executes. The default value is a null reference ( in Visual Basic). + + To be added. + + + + + + Method + + M:System.IDisposable.Dispose + + + System.Void + + + + Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + To be added. + + + + + + Method + + M:System.IAsyncDisposable.DisposeAsync + + + System.Threading.Tasks.ValueTask + + + + Asynchronously diposes the batch object. + + A representing the asynchronous operation. + + + + , are still thrown synchronously. For the stored exceptions, see the exceptions thrown by . + +]]> + + + + + + + + Method + + System.Data.Common.DbDataReader + + + + + + + An instance of , specifying options for batch execution and data retrieval. + + + When overridden in a derived class, executes the batch against its connection, returning a which can be used to access the results. + + + A object. + + + + can be used to advance the reader to the next result set. + + ]]> + + + An error occurred while executing the batch. + + The value is invalid. + + + + + + + Method + + System.Threading.Tasks.Task<System.Data.Common.DbDataReader> + + + + + + + One of the enumeration values that specifies options for batch execution and data retrieval. + A token to cancel the asynchronous operation. + + Providers should implement this method to provide a non-default implementation for overloads. + + The default implementation invokes the synchronous method and returns a completed task, blocking the calling thread. The default implementation will return a cancelled task if passed an already cancelled cancellation token. Exceptions thrown by ExecuteReader will be communicated via the returned Task Exception property. + + This method accepts a cancellation token that can be used to request the operation to be cancelled early. Implementations may ignore this request. + + A task representing the asynchronous operation. + + + , are still thrown synchronously. For the stored exceptions, see the exceptions thrown by . + +]]> + + + + + + + + Method + + System.Int32 + + + + Executes the batch against its connection object, returning the total number of rows affected across all the batch commands. + The total number of rows affected across all the batch commands. + + + to perform catalog operations (for example, querying the structure of a database or creating database objects such as tables), or to change the data in a database by executing UPDATE, INSERT, or DELETE statements. + + Although does not return any rows, any output parameters or return values mapped to parameters are populated with data. + + For UPDATE, INSERT, and DELETE statements, the return value is the total number of rows affected by the batch. If no UPDATE, INSERT, or DELETE statements are included in the batch, the return value is -1. + + ]]> + + + + + + + + Method + + System.Threading.Tasks.Task<System.Int32> + + + + + + A token to cancel the asynchronous operation. + + This is the asynchronous version of . Providers should override with an appropriate implementation. The cancellation token may optionally be ignored. + + The default implementation invokes the synchronous method and returns a completed task, blocking the calling thread. The default implementation will return a cancelled task if passed an already cancelled cancellation token. Exceptions thrown by will be communicated via the returned Task Exception property. + + Do not invoke other methods and properties of the object until the returned Task is complete. + + A task representing the asynchronous operation. + + + , are still thrown synchronously. For the stored exceptions, see the exceptions thrown by . + +]]> + + + An error occurred while executing the batch. + ADO.NET Overview + + + + + + Method + + System.Data.Common.DbDataReader + + + + + + One of the enumeration values that specifies options for batch execution and data retrieval. + + Executes the batch against its connection, returning a which can be used to access the results. + + + A object. + + + + can be used to advance the reader to the next result set. + + ]]> + + + + + + + + Method + + System.Threading.Tasks.Task<System.Data.Common.DbDataReader> + + + + + + To be added. + + An asynchronous version of , which executes the batch against its connection, returning a which can be used to access the results. + + A task representing the asynchronous operation. + + + in . For more information about asynchronous programming, see [Asynchronous Programming](/dotnet/framework/data/adonet/asynchronous-programming). + + ]]> + + + An error occurred while executing the batch. + + The value is invalid. + + + + + + + Method + + System.Threading.Tasks.Task<System.Data.Common.DbDataReader> + + + + + + + One of the enumeration values that specifies options for batch execution and data retrieval. + A token to cancel the asynchronous operation. + + An asynchronous version of , which executes the batch against its connection, returning a which can be used to access the results. + + A task representing the asynchronous operation. + + + in . For more information about asynchronous programming, see [Asynchronous Programming](/dotnet/framework/data/adonet/asynchronous-programming). + + This method stores in the task it returns all non-usage exceptions that the method's synchronous counterpart can throw. If an exception is stored into the returned task, that exception will be thrown when the task is awaited. Usage exceptions, such as , are still thrown synchronously. For the stored exceptions, see the exceptions thrown by . + +]]> + + + An error occurred while executing the batch. + + The value is invalid. + + + + + + + Method + + + [System.Runtime.CompilerServices.NullableContext(2)] + [<System.Runtime.CompilerServices.NullableContext(2)>] + + + + System.Object + + + + Executes the batch and returns the first column of the first row in the first returned result set. All other columns, rows and resultsets are ignored. + The first column of the first row in the first result set. + To be added. + An error occurred while executing the batch. + + + + + + Method + + System.Threading.Tasks.Task<System.Object> + + + + + + A token to cancel the asynchronous operation. + + An asynchronous version of , which executes the batch and returns the first column of the first row in the first returned result set. All other columns, rows and result sets are ignored. + + The first column of the first row in the first result set. + + This method stores in the task it returns all non-usage exceptions that the method's synchronous counterpart can throw. If an exception is stored into the returned task, that exception will be thrown when the task is awaited. Usage exceptions, such as , are still thrown synchronously. For the stored exceptions, see the exceptions thrown by . + + An error occurred while executing the batch. + + + + + + Method + + System.Void + + + + Creates a prepared (or compiled) version of the batch, or of each of its commands, on the data source. + To be added. + + + + + + Method + + System.Threading.Tasks.Task + + + + + + + An optional token to cancel the asynchronous operation. The default value is . + + Asynchronously creates a prepared (or compiled) version of the batch, or of each of its commands, on the data source. + + A representing the asynchronous operation. + + + This method stores in the task it returns all non-usage exceptions that the method's synchronous counterpart can throw. If an exception is stored into the returned task, that exception will be thrown when the task is awaited. Usage exceptions, such as , are still thrown synchronously. For the stored exceptions, see the exceptions thrown by . + + + + + + + Property + + System.Int32 + + + Gets or sets the wait time (in seconds) before terminating the attempt to execute the batch and generating an error. + The time in seconds to wait for the batch to execute. + + + is generated if the assigned property value is less than 0. + + Note to implementers: it's recommended that 0 mean no timeout. + + ]]> + + + + + + + + Property + + + [System.Runtime.CompilerServices.Nullable(2)] + [<System.Runtime.CompilerServices.Nullable(2)>] + + + [get: System.Runtime.CompilerServices.NullableContext(2)] + [<get: System.Runtime.CompilerServices.NullableContext(2)>] + + + [set: System.Runtime.CompilerServices.NullableContext(2)] + [<set: System.Runtime.CompilerServices.NullableContext(2)>] + + + + System.Data.Common.DbTransaction + + + + Gets or sets the within which this object executes. + + + The transaction within which a batch of a .NET data provider executes. The default value is a null reference ( in Visual Basic). + + To be added. + + + + diff --git a/doc/snippets/Microsoft.Data.Common/DbBatchCommand.xml b/doc/snippets/Microsoft.Data.Common/DbBatchCommand.xml new file mode 100644 index 0000000000..68efa39676 --- /dev/null +++ b/doc/snippets/Microsoft.Data.Common/DbBatchCommand.xml @@ -0,0 +1,113 @@ + + + + + System.Object + + + + + [System.Runtime.CompilerServices.Nullable(0)] + [<System.Runtime.CompilerServices.Nullable(0)>] + + + [System.Runtime.CompilerServices.NullableContext(1)] + [<System.Runtime.CompilerServices.NullableContext(1)>] + + + + + Represents a single command within a . A batch can be executed against a data source in a single round trip. + + To be added. + + + + + + Constructor + + + + Constructs an instance of the object. + + To be added. + + + + + + Property + + System.String + + + Gets or sets the text command to run against the data source. + The text command to execute. The default value is an empty string (""). + To be added. + + + + + + Property + + System.Data.CommandType + + + + Gets or sets how the property is interpreted. + + + One of the enumeration values that specifies how a command string is interpreted. The default is . + + To be added. + + + + + + Property + + System.Data.Common.DbParameterCollection + + + + Gets the collection of objects. + + The parameters of the SQL statement or stored procedure. + To be added. + + + + + + Property + + System.Data.Common.DbParameterCollection + + + + Gets the collection of objects. For more information on parameters, see [Configuring Parameters and Parameter Data Types](/dotnet/framework/data/adonet/configuring-parameters-and-parameter-data-types). + + The parameters of the SQL statement or stored procedure. + To be added. + + + + + + Property + + System.Int32 + + + + Gets the number of rows changed, inserted, or deleted by execution of this specific . + + The number of rows changed, inserted, or deleted. -1 for SELECT statements; 0 if no rows were affected or the statement failed. + To be added. + + + + diff --git a/doc/snippets/Microsoft.Data.Common/DbBatchCommandCollection.xml b/doc/snippets/Microsoft.Data.Common/DbBatchCommandCollection.xml new file mode 100644 index 0000000000..610c7860f7 --- /dev/null +++ b/doc/snippets/Microsoft.Data.Common/DbBatchCommandCollection.xml @@ -0,0 +1,410 @@ + + + + + System.Object + + + + System.Collections.Generic.ICollection<Microsoft.Data.Common.DbBatchCommand> + + + System.Collections.Generic.ICollection<T> + + + System.Collections.Generic.IEnumerable<Microsoft.Data.Common.DbBatchCommand> + + + System.Collections.Generic.IEnumerable<T> + + + System.Collections.Generic.IList<Microsoft.Data.Common.DbBatchCommand> + + + System.Collections.IEnumerable + + + + + [System.Runtime.CompilerServices.Nullable(0)] + [<System.Runtime.CompilerServices.Nullable(0)>] + + + [System.Runtime.CompilerServices.NullableContext(1)] + [<System.Runtime.CompilerServices.NullableContext(1)>] + + + + + The base class for a collection of instances of , contained in a . + + To be added. + + + + + Constructor + + + + Initializes a new instance of the class. + + To be added. + + + + + + Method + + M:System.Collections.Generic.ICollection`1.Add(`0) + + + System.Void + + + + + + + The object to add to the . + + + Adds the specified object to the . + + To be added. + + + + + + Method + + M:System.Collections.Generic.ICollection`1.Clear + + + System.Void + + + + + Removes all values from the . + + To be added. + + + + + + Method + + M:System.Collections.Generic.ICollection`1.Contains(`0) + + + System.Boolean + + + + + + + The object to locate in the . + + + Indicates whether a is contained in the collection. + + + if the is in the collection; otherwise . + + To be added. + + + + + + Method + + M:System.Collections.Generic.ICollection`1.CopyTo(`0[],System.Int32) + + + System.Void + + + + + + + + The one-dimensional that is the destination of the elements copied from . The must have zero-based indexing. + + + The zero-based index in at which copying begins. + + + Copies the elements of the to an , starting at a particular index. + + To be added. + + + + + + Property + + P:System.Collections.Generic.ICollection`1.Count + + + System.Int32 + + + + Gets the number of elements contained in the . + + + The number of elements contained in the . + + To be added. + + + + + + Method + + Microsoft.Data.Common.DbBatchCommand + + + + + + To be added. + To be added. + To be added. + To be added. + + + + + + Method + + M:System.Collections.Generic.IEnumerable`1.GetEnumerator + + + System.Collections.Generic.IEnumerator<Microsoft.Data.Common.DbBatchCommand> + + + + + Returns the object at the specified index in the collection. + + + The object at the specified index in the collection. + + To be added. + + + + + + Method + + M:System.Collections.Generic.IList`1.IndexOf(`0) + + + System.Int32 + + + + + + + The object to locate in the . + + + Returns the index of the specified object. + + + The index of the specified object. + + To be added. + + + + + + + + + + Method + + M:System.Collections.Generic.IList`1.Insert(System.Int32,`0) + + + System.Void + + + + + + + + The index at which to insert the object. + + + The object to insert into the . + + + Inserts the specified index of the object with the specified name into the collection at the specified index. + + To be added. + + + + + + Property + + P:System.Collections.Generic.ICollection`1.IsReadOnly + + + System.Boolean + + + Specifies whether the collection is read-only. + + if the collection is read-only; otherwise . + + To be added. + + + + + + Property + + P:System.Collections.Generic.IList`1.Item(System.Int32) + + + Microsoft.Data.Common.DbBatchCommand + + + + + + + The zero-based index of the . + + + Gets or sets the at the specified index. + + + The at the specified index. + + To be added. + The specified index does not exist. + + + + + + Method + + M:System.Collections.Generic.ICollection`1.Remove(`0) + + + System.Boolean + + + + + + + The object to remove from the . + + + Removes the specified object from the collection. + + + if was successfully removed; otherwise, . This method also returns if was not found in the . + + To be added. + + + + + + Method + + M:System.Collections.Generic.IList`1.RemoveAt(System.Int32) + + + System.Void + + + + + + + The index where the object is located. + + + Removes the object at the specified from the collection. + + To be added. + + + + + + Method + + System.Void + + + + + + + + The index where the object is located. + + To be added. + + Sets the object at the specified index to a new value. + + To be added. + + + + + + Method + + M:System.Collections.IEnumerable.GetEnumerator + + + System.Collections.IEnumerator + + + + Returns an enumerator that iterates through a collection. + + An object that can be used to iterate through the collection. + + + + instance is cast to an interface. + + ]]> + + + + + + diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlBatch.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlBatch.xml new file mode 100644 index 0000000000..476a7a492e --- /dev/null +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlBatch.xml @@ -0,0 +1,145 @@ + + + + + + + + and a , then adds multiple objects to the batch. It then executes the batch, creating a . The example reads through the results of the batch commands, writing them to the console. Finally, the example closes the and then the as the `using` blocks fall out of scope. + +[!code-csharp[SqlCommand Example#1](~/../sqlclient/doc/samples/SqlBatch_ExecuteReader.cs#1)] +]]> + + + + + + Initializes a new . + + + + + and a , then adds multiple objects to the batch. It then executes the batch, creating a . The example reads through the results of the batch commands, writing them to the console. Finally, the example closes the and then the as the `using` blocks fall out of scope. + +[!code-csharp[SqlCommand Example#1](~/../sqlclient/doc/samples/SqlBatch_ExecuteReader.cs#1)] +]]> + + + + + Initializes a new . + + + A + + that represents the connection to an instance of SQL Server. + + + The + + in which the + + executes. + + + + + + + + + The list of commands contained in the batch in a . + + + + and a , then adds multiple objects to the batch. It then executes the batch, creating a . The example reads through the results of the batch commands, writing them to the console. Finally, the example closes the and then the as the `using` blocks fall out of scope. + +[!code-csharp[SqlCommand Example#1](~/../sqlclient/doc/samples/SqlBatch_ExecuteReader.cs#1)] +]]> + + + + + + The list of commands contained in the batch in a . + + + + + + + + + + Gets or sets the + + used by this instance of the + + . + + + + + + + + + Gets or sets the within which the commands execute. + + + + + + + + + Sends the + + to the + + and builds a + + . + + + + and a , then adds multiple objects to the batch. It then executes the batch, creating a . The example reads through the results of the batch commands, writing them to the console. Finally, the example closes the and then the as the `using` blocks fall out of scope. + +[!code-csharp[SqlCommand Example#1](~/../sqlclient/doc/samples/SqlBatch_ExecuteReader.cs#1)] +]]> + + + + + + An asynchronous version of + + , which sends the + + to the + + and builds a + + . Exceptions will be reported via the returned Task object. + + + + + + + + + diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlBatchCommand.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlBatchCommand.xml new file mode 100644 index 0000000000..9ad24b8abf --- /dev/null +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlBatchCommand.xml @@ -0,0 +1,76 @@ + + + + + Initializes a new . + + + and a , then adds multiple objects to the batch. It then executes the batch, creating a . The example reads through the results of the batch commands, writing them to the console. Finally, the example closes the and then the as the `using` blocks fall out of scope. + +[!code-csharp[SqlCommand Example#1](~/../sqlclient/doc/samples/SqlBatch_ExecuteReader.cs#1)] +]]> + + + + + Initializes a new . + The text of the . + Indicates how the property is to be interpreted. + A , which is used to crate the . + The encryption setting. For more information, see [Always Encrypted](/sql/relational-databases/security/encryption/always-encrypted-database-engine). + + + + + + + + + Gets the + + . + + The parameters of the Transact-SQL statement or stored procedure. The default is an empty collection. + + + + + + + + + One of the values, indicating options for statement execution and data retrieval. + + + + + + + + + + Not currently implemented. + The encryption setting. For more information, see [Always Encrypted](/sql/relational-databases/security/encryption/always-encrypted-database-engine). + + + + + + + + + Creates a new instance of a object. + + + + Returns whether the method is implemented. + + + + diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlBatchCommandCollection.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlBatchCommandCollection.xml new file mode 100644 index 0000000000..f350a48731 --- /dev/null +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlBatchCommandCollection.xml @@ -0,0 +1,99 @@ + + + + + A collection of instances of , contained within a . + + + + + + + + + Add a to the end of the . + + + + + + + + + Determines whether a is in the . + + + + + + + + + Copies the entire to a one dimensional array, starting at the target index of the target array. + + + + + + + + + Searches for the specified within the and returns the zero-based index of the first occurence within the entire . + + Returns the zero-based index of the first occurence within the entire . + + + + + + + + + Inserts an item into the at the specified index. + + + + + + + + + Removes the first occurence of the specific item from the . + Returns if an item is successfully removed. Returns false if an item could not be removed or no item was not found. + + + + + + + + Gets or Sets the element at the specified index. + The element at the specified index. + + + + + + + + + Gets or Sets the element at the specified index. + The element at the specified index. + + + + + + + + + diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlClientFactory.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlClientFactory.xml index 6408d55a11..3947acf211 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlClientFactory.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlClientFactory.xml @@ -38,6 +38,31 @@ The following example displays a list of all available SQL Server data sources, ]]> + + + Gets a value that indicates whether a + + can be created. + + + + if a + + can be created; otherwise, + + . + + + + class provides the property so that inheritors can indicate +whether they can provide a DbBatch. The displays this property, but its value is always `true`. +]]> + + + Returns a strongly typed @@ -227,6 +252,60 @@ DbParameter cmd = newFactory.CreateParameter(); To be added. + + + Returns a strongly typed + + instance. + + + A new strongly typed instance of + + . + + + + instance: + + +```csharp +SqlClientFactory newFactory = SqlClientFactory.Instance; +DbParameter cmd = newFactory.CreateBatch(); +``` +]]> + + + + + + Returns a strongly typed + + instance. + + + A new strongly typed instance of + + . + + + + instance: + + +```csharp +SqlClientFactory newFactory = SqlClientFactory.Instance; +DbParameter cmd = newFactory.CreateBatchCommand(); +``` +]]> + + + Gets an instance of the diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlException.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlException.xml index 9bb1ef18f3..b07ef7be4c 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlException.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlException.xml @@ -292,6 +292,17 @@ catch (Exception ex) { + + Gets the BatchCommand instance that generated the error or null if the exception was not raised from a batch. + A BatchCommand object or null + + + + + + + + Gets a numeric error code from SQL Server that represents an error, warning or "no data found" message. For more information about how to decode these values, see Database Engine Events and Errors. The number representing the error code. diff --git a/src/Microsoft.Data.SqlClient.sln b/src/Microsoft.Data.SqlClient.sln index 327b6f4ff9..e1022d786e 100644 --- a/src/Microsoft.Data.SqlClient.sln +++ b/src/Microsoft.Data.SqlClient.sln @@ -98,6 +98,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Microsoft.Data.SqlClient", ..\doc\snippets\Microsoft.Data.SqlClient\SqlAuthenticationParameters.xml = ..\doc\snippets\Microsoft.Data.SqlClient\SqlAuthenticationParameters.xml ..\doc\snippets\Microsoft.Data.SqlClient\SqlAuthenticationProvider.xml = ..\doc\snippets\Microsoft.Data.SqlClient\SqlAuthenticationProvider.xml ..\doc\snippets\Microsoft.Data.SqlClient\SqlAuthenticationToken.xml = ..\doc\snippets\Microsoft.Data.SqlClient\SqlAuthenticationToken.xml + ..\doc\snippets\Microsoft.Data.SqlClient\SqlBatch.xml = ..\doc\snippets\Microsoft.Data.SqlClient\SqlBatch.xml + ..\doc\snippets\Microsoft.Data.SqlClient\SqlBatchCommand.xml = ..\doc\snippets\Microsoft.Data.SqlClient\SqlBatchCommand.xml + ..\doc\snippets\Microsoft.Data.SqlClient\SqlBatchCommandCollection.xml = ..\doc\snippets\Microsoft.Data.SqlClient\SqlBatchCommandCollection.xml ..\doc\snippets\Microsoft.Data.SqlClient\SqlBulkCopy.xml = ..\doc\snippets\Microsoft.Data.SqlClient\SqlBulkCopy.xml ..\doc\snippets\Microsoft.Data.SqlClient\SqlBulkCopyColumnMapping.xml = ..\doc\snippets\Microsoft.Data.SqlClient\SqlBulkCopyColumnMapping.xml ..\doc\snippets\Microsoft.Data.SqlClient\SqlBulkCopyColumnMappingCollection.xml = ..\doc\snippets\Microsoft.Data.SqlClient\SqlBulkCopyColumnMappingCollection.xml diff --git a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj index 96308eae65..4bee03a8e0 100644 --- a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.csproj @@ -16,12 +16,16 @@ - - + + + + + + diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj index c094cf766d..21454d70ec 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -273,6 +273,15 @@ Microsoft\Data\SqlClient\SqlAuthenticationToken.cs + + Microsoft\Data\SqlClient\SqlBatch.cs + + + Microsoft\Data\SqlClient\SqlBatchCommand.cs + + + Microsoft\Data\SqlClient\SqlBatchCommandCollection.cs + Microsoft\Data\SqlClient\SqlBulkCopyColumnMapping.cs @@ -956,6 +965,14 @@ Common\System\Net\Security\NegotiateStreamPal.Unix.cs + + + + + + Microsoft\Data\SqlClient\SqlBatchCommand.Net8OrGreater.cs + + @@ -991,4 +1008,4 @@ - + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlClientFactory.NetCoreApp.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlClientFactory.NetCoreApp.cs new file mode 100644 index 0000000000..4b0d4a8b2d --- /dev/null +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlClientFactory.NetCoreApp.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Data.Common; + +namespace Microsoft.Data.SqlClient +{ + public sealed partial class SqlClientFactory : DbProviderFactory + { + /// + public override bool CanCreateBatch => true; + + /// + public override DbBatch CreateBatch() => new SqlBatch(); + + /// + public override DbBatchCommand CreateBatchCommand() => new SqlBatchCommand(); + } +} diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs index 3e12b61a61..39dbb917e0 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -391,9 +391,7 @@ private CachedAsyncState cachedAsyncState private bool _batchRPCMode; private List<_SqlRPC> _RPCList; - private _SqlRPC[] _SqlRPCBatchArray; private _SqlRPC[] _sqlRPCParameterEncryptionReqArray; - private List _parameterCollectionList; private int _currentlyExecutingBatch; private SqlRetryLogicBaseProvider _retryLogicProvider; @@ -1142,7 +1140,7 @@ public override object ExecuteScalar() RunExecuteReaderWithRetry(0, RunBehavior.ReturnImmediately, returnStream: true) : RunExecuteReader(0, RunBehavior.ReturnImmediately, returnStream: true, method: nameof(ExecuteScalar)); success = true; - return CompleteExecuteScalar(ds, false); + return CompleteExecuteScalar(ds, _batchRPCMode); } catch (Exception ex) { @@ -1161,26 +1159,22 @@ public override object ExecuteScalar() } } - private object CompleteExecuteScalar(SqlDataReader ds, bool returnSqlValue) + private object CompleteExecuteScalar(SqlDataReader ds, bool returnLastResult) { object retResult = null; try { - if (ds.Read()) + do { - if (ds.FieldCount > 0) + if (ds.Read()) { - if (returnSqlValue) - { - retResult = ds.GetSqlValue(0); - } - else + if (ds.FieldCount > 0) { retResult = ds.GetValue(0); } } - } + } while (returnLastResult && ds.NextResult()); } finally { @@ -1407,7 +1401,7 @@ private void VerifyEndExecuteState(Task completionTask, string endMethod, bool f { _stateObj.Parser.State = TdsParserState.Broken; // We failed to respond to attention, we have to quit! _stateObj.Parser.Connection.BreakConnection(); - _stateObj.Parser.ThrowExceptionAndWarning(_stateObj); + _stateObj.Parser.ThrowExceptionAndWarning(_stateObj, this); } else { @@ -1678,7 +1672,7 @@ private Task InternalExecuteNonQuery(TaskCompletionSource completion, bo // Always Encrypted generally operates only on parameterized queries. However enclave based Always encrypted also supports unparameterized queries // We skip this block for enclave based always encrypted so that we can make a call to SQL Server to get the encryption information - if (!ShouldUseEnclaveBasedWorkflow && !BatchRPCMode && (CommandType.Text == CommandType) && (0 == GetParameterCount(_parameters))) + if (!ShouldUseEnclaveBasedWorkflow && !_batchRPCMode && (CommandType.Text == CommandType) && (0 == GetParameterCount(_parameters))) { Debug.Assert(!sendToPipe, "Trying to send non-context command to pipe"); @@ -2912,6 +2906,94 @@ private Task InternalExecuteScalarAsync(CancellationToken cancellationTo }, TaskScheduler.Default).Unwrap(); } + internal Task ExecuteScalarBatchAsync(CancellationToken cancellationToken) + { + _parentOperationStarted = true; + Guid operationId = s_diagnosticListener.WriteCommandBefore(this, _transaction); + + return ExecuteReaderAsync(cancellationToken).ContinueWith((executeTask) => + { + TaskCompletionSource source = new TaskCompletionSource(); + if (executeTask.IsCanceled) + { + source.SetCanceled(); + } + else if (executeTask.IsFaulted) + { + s_diagnosticListener.WriteCommandError(operationId, this, _transaction, executeTask.Exception.InnerException); + source.SetException(executeTask.Exception.InnerException); + } + else + { + SqlDataReader reader = executeTask.Result; + ExecuteScalarUntilEndAsync(reader, cancellationToken).ContinueWith( + (readTask) => + { + try + { + if (readTask.IsCanceled) + { + reader.Dispose(); + source.SetCanceled(); + } + else if (readTask.IsFaulted) + { + reader.Dispose(); + s_diagnosticListener.WriteCommandError(operationId, this, _transaction, readTask.Exception.InnerException); + source.SetException(readTask.Exception.InnerException); + } + else + { + Exception exception = null; + object result = null; + try + { + result = readTask.Result; + } + finally + { + reader.Dispose(); + } + if (exception != null) + { + s_diagnosticListener.WriteCommandError(operationId, this, _transaction, exception); + source.SetException(exception); + } + else + { + s_diagnosticListener.WriteCommandAfter(operationId, this, _transaction); + source.SetResult(result); + } + } + } + catch (Exception e) + { + // exception thrown by Dispose... + source.SetException(e); + } + }, + TaskScheduler.Default + ); + } + _parentOperationStarted = false; + return source.Task; + }, TaskScheduler.Default).Unwrap(); + } + + private async Task ExecuteScalarUntilEndAsync(SqlDataReader reader, CancellationToken cancellationToken) + { + object retval = null; + do + { + if (await reader.ReadAsync(cancellationToken).ConfigureAwait(false) && reader.FieldCount > 0) + { + retval = reader.GetValue(0); // no async untyped value getter, this will work ok as long as the value is in the current packet + } + } + while (_batchRPCMode && !cancellationToken.IsCancellationRequested && await reader.NextResultAsync(cancellationToken).ConfigureAwait(false)); + return retval; + } + /// public Task ExecuteXmlReaderAsync() { @@ -3759,7 +3841,7 @@ private void PrepareForTransparentEncryption(CommandBehavior cmdBehavior, bool r // If we are not in Batch RPC and not already retrying, attempt to fetch the cipher MD for each parameter from the cache. // If this succeeds then return immediately, otherwise just fall back to the full crypto MD discovery. - if (!BatchRPCMode && !inRetry && (this._parameters != null && this._parameters.Count > 0) && SqlQueryMetadataCache.GetInstance().GetQueryMetadataIfExists(this)) + if (!_batchRPCMode && !inRetry && (this._parameters != null && this._parameters.Count > 0) && SqlQueryMetadataCache.GetInstance().GetQueryMetadataIfExists(this)) { usedCache = true; return; @@ -3774,7 +3856,7 @@ private void PrepareForTransparentEncryption(CommandBehavior cmdBehavior, bool r // Flag to indicate if exception is caught during the execution, to govern clean up. bool exceptionCaught = false; - // Used in BatchRPCMode to maintain a map of describe parameter encryption RPC requests (Keys) and their corresponding original RPC requests (Values). + // Used in _batchRPCMode to maintain a map of describe parameter encryption RPC requests (Keys) and their corresponding original RPC requests (Values). ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap = null; @@ -3797,8 +3879,8 @@ private void PrepareForTransparentEncryption(CommandBehavior cmdBehavior, bool r Debug.Assert(fetchInputParameterEncryptionInfoTask == null || isAsync, "Task returned by TryFetchInputParameterEncryptionInfo, when in sync mode, in PrepareForTransparentEncryption."); - Debug.Assert((describeParameterEncryptionRpcOriginalRpcMap != null) == BatchRPCMode, - "describeParameterEncryptionRpcOriginalRpcMap can be non-null if and only if it is in BatchRPCMode."); + Debug.Assert((describeParameterEncryptionRpcOriginalRpcMap != null) == _batchRPCMode, + "describeParameterEncryptionRpcOriginalRpcMap can be non-null if and only if it is in _batchRPCMode."); // If we didn't have parameters, we can fall back to regular code path, by simply returning. if (!describeParameterEncryptionNeeded) @@ -4077,32 +4159,32 @@ private SqlDataReader TryFetchInputParameterEncryptionInfo(int timeout, } } - if (BatchRPCMode) + if (_batchRPCMode) { // Count the rpc requests that need to be transparently encrypted // We simply look for any parameters in a request and add the request to be queried for parameter encryption Dictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcDictionary = new Dictionary<_SqlRPC, _SqlRPC>(); - for (int i = 0; i < _SqlRPCBatchArray.Length; i++) + for (int i = 0; i < _RPCList.Count; i++) { // In BatchRPCMode, the actual T-SQL query is in the first parameter and not present as the rpcName, as is the case with non-BatchRPCMode. // So input parameters start at parameters[1]. parameters[0] is the actual T-SQL Statement. rpcName is sp_executesql. - if (_SqlRPCBatchArray[i].systemParams.Length > 1) + if (_RPCList[i].systemParams.Length > 1) { - _SqlRPCBatchArray[i].needsFetchParameterEncryptionMetadata = true; + _RPCList[i].needsFetchParameterEncryptionMetadata = true; // Since we are going to need multiple RPC objects, allocate a new one here for each command in the batch. _SqlRPC rpcDescribeParameterEncryptionRequest = new _SqlRPC(); // Prepare the describe parameter encryption request. - PrepareDescribeParameterEncryptionRequest(_SqlRPCBatchArray[i], ref rpcDescribeParameterEncryptionRequest, i == 0 ? serializedAttestationParameters : null); + PrepareDescribeParameterEncryptionRequest(_RPCList[i], ref rpcDescribeParameterEncryptionRequest, i == 0 ? serializedAttestationParameters : null); Debug.Assert(rpcDescribeParameterEncryptionRequest != null, "rpcDescribeParameterEncryptionRequest should not be null, after call to PrepareDescribeParameterEncryptionRequest."); Debug.Assert(!describeParameterEncryptionRpcOriginalRpcDictionary.ContainsKey(rpcDescribeParameterEncryptionRequest), "There should not already be a key referring to the current rpcDescribeParameterEncryptionRequest, in the dictionary describeParameterEncryptionRpcOriginalRpcDictionary."); // Add the describe parameter encryption RPC request as the key and its corresponding original rpc request to the dictionary. - describeParameterEncryptionRpcOriginalRpcDictionary.Add(rpcDescribeParameterEncryptionRequest, _SqlRPCBatchArray[i]); + describeParameterEncryptionRpcOriginalRpcDictionary.Add(rpcDescribeParameterEncryptionRequest, _RPCList[i]); } } @@ -4122,7 +4204,7 @@ private SqlDataReader TryFetchInputParameterEncryptionInfo(int timeout, describeParameterEncryptionRpcOriginalRpcMap.Keys.CopyTo(_sqlRPCParameterEncryptionReqArray, 0); Debug.Assert(_sqlRPCParameterEncryptionReqArray.Length > 0, "There should be at-least 1 describe parameter encryption rpc request."); - Debug.Assert(_sqlRPCParameterEncryptionReqArray.Length <= _SqlRPCBatchArray.Length, + Debug.Assert(_sqlRPCParameterEncryptionReqArray.Length <= _RPCList.Count, "The number of decribe parameter encryption RPC requests is more than the number of original RPC requests."); } //Always Encrypted generally operates only on parameterized queries. However enclave based Always encrypted also supports unparameterized queries @@ -4193,11 +4275,11 @@ private void PrepareDescribeParameterEncryptionRequest(_SqlRPC originalRpcReques // Prepare @tsql parameter string text; - // In BatchRPCMode, The actual T-SQL query is in the first parameter and not present as the rpcName, as is the case with non-BatchRPCMode. - if (BatchRPCMode) + // In _batchRPCMode, The actual T-SQL query is in the first parameter and not present as the rpcName, as is the case with non-_batchRPCMode. + if (_batchRPCMode) { Debug.Assert(originalRpcRequest.systemParamCount > 0, - "originalRpcRequest didn't have at-least 1 parameter in BatchRPCMode, in PrepareDescribeParameterEncryptionRequest."); + "originalRpcRequest didn't have at-least 1 parameter in _batchRPCMode, in PrepareDescribeParameterEncryptionRequest."); text = (string)originalRpcRequest.systemParams[0].Value; //@tsql SqlParameter tsqlParam = describeParameterEncryptionRequest.systemParams[0]; @@ -4231,7 +4313,7 @@ private void PrepareDescribeParameterEncryptionRequest(_SqlRPC originalRpcReques // In BatchRPCMode, the input parameters start at parameters[1]. parameters[0] is the T-SQL statement. rpcName is sp_executesql. // And it is already in the format expected out of BuildParamList, which is not the case with Non-BatchRPCMode. - if (BatchRPCMode) + if (_batchRPCMode) { // systemParamCount == 2 when user parameters are supplied to BuildExecuteSql if (originalRpcRequest.systemParamCount > 1) @@ -4324,8 +4406,8 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi SqlTceCipherInfoEntry cipherInfoEntry; Dictionary columnEncryptionKeyTable = new Dictionary(); - Debug.Assert((describeParameterEncryptionRpcOriginalRpcMap != null) == BatchRPCMode, - "describeParameterEncryptionRpcOriginalRpcMap should be non-null if and only if it is BatchRPCMode."); + Debug.Assert((describeParameterEncryptionRpcOriginalRpcMap != null) == _batchRPCMode, + "describeParameterEncryptionRpcOriginalRpcMap should be non-null if and only if it is _batchRPCMode."); // Indicates the current result set we are reading, used in BatchRPCMode, where we can have more than 1 result set. int resultSetSequenceNumber = 0; @@ -4341,12 +4423,12 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi do { - if (BatchRPCMode) + if (_batchRPCMode) { // If we got more RPC results from the server than what was requested. if (resultSetSequenceNumber >= _sqlRPCParameterEncryptionReqArray.Length) { - Debug.Assert(false, "Server sent back more results than what was expected for describe parameter encryption requests in BatchRPCMode."); + Debug.Assert(false, "Server sent back more results than what was expected for describe parameter encryption requests in _batchRPCMode."); // Ignore the rest of the results from the server, if for whatever reason it sends back more than what we expect. break; } @@ -4463,7 +4545,7 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi // Find the RPC command that generated this tce request - if (BatchRPCMode) + if (_batchRPCMode) { Debug.Assert(_sqlRPCParameterEncryptionReqArray[resultSetSequenceNumber] != null, "_sqlRPCParameterEncryptionReqArray[resultSetSequenceNumber] should not be null."); @@ -4536,8 +4618,8 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi Debug.Assert(_activeConnection != null, @"_activeConnection should not be null"); SqlSecurityUtility.DecryptSymmetricKey(sqlParameter.CipherMetadata, _activeConnection, this); - // This is effective only for BatchRPCMode even though we set it for non-BatchRPCMode also, - // since for non-BatchRPCMode mode, paramoptions gets thrown away and reconstructed in BuildExecuteSql. + // This is effective only for _batchRPCMode even though we set it for non-_batchRPCMode also, + // since for non-_batchRPCMode mode, paramoptions gets thrown away and reconstructed in BuildExecuteSql. int options = (int)(rpc.userParamMap[index] >> 32); options |= TdsEnums.RPC_PARAM_ENCRYPTED; rpc.userParamMap[index] = ((((long)options) << 32) | (long)index); @@ -4621,19 +4703,19 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi } while (ds.NextResult()); // Verify that we received response for each rpc call needs tce - if (BatchRPCMode) + if (_batchRPCMode) { - for (int i = 0; i < _SqlRPCBatchArray.Length; i++) + for (int i = 0; i < _RPCList.Count; i++) { - if (_SqlRPCBatchArray[i].needsFetchParameterEncryptionMetadata) + if (_RPCList[i].needsFetchParameterEncryptionMetadata) { - throw SQL.ProcEncryptionMetadataMissing(_SqlRPCBatchArray[i].rpcName); + throw SQL.ProcEncryptionMetadataMissing(_RPCList[i].rpcName); } } } // If we are not in Batch RPC mode, update the query cache with the encryption MD. - if (!BatchRPCMode && ShouldCacheEncryptionMetadata && (_parameters is not null && _parameters.Count > 0)) + if (!_batchRPCMode && ShouldCacheEncryptionMetadata && (_parameters is not null && _parameters.Count > 0)) { SqlQueryMetadataCache.GetInstance().AddQueryMetadata(this, ignoreQueriesWithReturnValueParams: true); } @@ -4949,13 +5031,13 @@ private SqlDataReader RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavi Debug.Assert(_sqlRPCParameterEncryptionReqArray != null, "RunExecuteReader rpc array not provided for describe parameter encryption request."); writeTask = _stateObj.Parser.TdsExecuteRPC(this, _sqlRPCParameterEncryptionReqArray, timeout, inSchema, this.Notification, _stateObj, CommandType.StoredProcedure == CommandType, sync: !asyncWrite); } - else if (BatchRPCMode) + else if (_batchRPCMode) { Debug.Assert(inSchema == false, "Batch RPC does not support schema only command behavior"); Debug.Assert(!IsPrepared, "Batch RPC should not be prepared!"); Debug.Assert(!IsDirty, "Batch RPC should not be marked as dirty!"); - Debug.Assert(_SqlRPCBatchArray != null, "RunExecuteReader rpc array not provided"); - writeTask = _stateObj.Parser.TdsExecuteRPC(this, _SqlRPCBatchArray, timeout, inSchema, this.Notification, _stateObj, CommandType.StoredProcedure == CommandType, sync: !asyncWrite); + Debug.Assert(_RPCList != null, "RunExecuteReader rpc array not provided"); + writeTask = _stateObj.Parser.TdsExecuteRPC(this, _RPCList, timeout, inSchema, this.Notification, _stateObj, CommandType.StoredProcedure == CommandType, sync: !asyncWrite); } else if ((CommandType.Text == this.CommandType) && (0 == GetParameterCount(_parameters))) { @@ -5371,21 +5453,29 @@ private void ValidateCommand(bool isAsync, [CallerMemberName] string method = "" // Check to see if the currently set transaction has completed. If so, // null out our local reference. if (null != _transaction && _transaction.Connection == null) + { _transaction = null; + } // throw if the connection is in a transaction but there is no // locally assigned transaction object if (_activeConnection.HasLocalTransactionFromAPI && (null == _transaction)) + { throw ADP.TransactionRequired(method); + } // if we have a transaction, check to ensure that the active // connection property matches the connection associated with // the transaction if (null != _transaction && _activeConnection != _transaction.Connection) + { throw ADP.TransactionConnectionMismatch(); + } if (string.IsNullOrEmpty(this.CommandText)) + { throw ADP.CommandTextRequired(method); + } } private void ValidateAsyncCommand() @@ -5467,28 +5557,28 @@ private void PutStateObject() internal void OnDoneDescribeParameterEncryptionProc(TdsParserStateObject stateObj) { // called per rpc batch complete - if (BatchRPCMode) + if (_batchRPCMode) { OnDone(stateObj, _currentlyExecutingDescribeParameterEncryptionRPC, _sqlRPCParameterEncryptionReqArray, _rowsAffected); _currentlyExecutingDescribeParameterEncryptionRPC++; } } - internal void OnDoneProc() + internal void OnDoneProc(TdsParserStateObject stateObject) { // called per rpc batch complete - if (BatchRPCMode) + if (_batchRPCMode) { - OnDone(_stateObj, _currentlyExecutingBatch, _SqlRPCBatchArray, _rowsAffected); + OnDone(stateObject, _currentlyExecutingBatch, _RPCList, _rowsAffected); _currentlyExecutingBatch++; - Debug.Assert(_parameterCollectionList.Count >= _currentlyExecutingBatch, "OnDoneProc: Too many DONEPROC events"); + Debug.Assert(_RPCList.Count >= _currentlyExecutingBatch, "OnDoneProc: Too many DONEPROC events"); } } - private static void OnDone(TdsParserStateObject stateObj, int index, _SqlRPC[] array, int rowsAffected) + private static void OnDone(TdsParserStateObject stateObj, int index, IList<_SqlRPC> rpcList, int rowsAffected) { - _SqlRPC current = array[index]; - _SqlRPC previous = (index > 0) ? array[index - 1] : null; + _SqlRPC current = rpcList[index]; + _SqlRPC previous = (index > 0) ? rpcList[index - 1] : null; // track the records affected for the just completed rpc batch // _rowsAffected is cumulative for ExecuteNonQuery across all rpc batches @@ -5499,6 +5589,11 @@ private static void OnDone(TdsParserStateObject stateObj, int index, _SqlRPC[] a ? (rowsAffected - Math.Max(previous.cumulativeRecordsAffected, 0)) : rowsAffected); + if (current.batchCommand != null) + { + current.batchCommand.SetRecordAffected(current.recordsAffected.GetValueOrDefault()); + } + // track the error collection (not available from TdsParser after ExecuteNonQuery) // and the which errors are associated with the just completed rpc batch current.errorsIndexStart = previous?.errorsIndexEnd ?? 0; @@ -5521,11 +5616,11 @@ internal void OnReturnStatus(int status) } SqlParameterCollection parameters = _parameters; - if (BatchRPCMode) + if (_batchRPCMode) { - if (_parameterCollectionList.Count > _currentlyExecutingBatch) + if (_RPCList.Count > _currentlyExecutingBatch) { - parameters = _parameterCollectionList[_currentlyExecutingBatch]; + parameters = _RPCList[_currentlyExecutingBatch].userParams; } else { @@ -5555,7 +5650,7 @@ internal void OnReturnStatus(int status) // If we are not in Batch RPC mode, update the query cache with the encryption MD. // We can do this now that we have distinguished between ReturnValue and ReturnStatus. // Read comment in AddQueryMetadata() for more details. - if (!BatchRPCMode && CachingQueryMetadataPostponed && + if (!_batchRPCMode && CachingQueryMetadataPostponed && ShouldCacheEncryptionMetadata && (_parameters is not null && _parameters.Count > 0)) { SqlQueryMetadataCache.GetInstance().AddQueryMetadata(this, ignoreQueriesWithReturnValueParams: false); @@ -5728,11 +5823,11 @@ internal void OnReturnValue(SqlReturnValue rec, TdsParserStateObject stateObj) private SqlParameterCollection GetCurrentParameterCollection() { - if (BatchRPCMode) + if (_batchRPCMode) { - if (_parameterCollectionList.Count > _currentlyExecutingBatch) + if (_RPCList.Count > _currentlyExecutingBatch) { - return _parameterCollectionList[_currentlyExecutingBatch]; + return _RPCList[_currentlyExecutingBatch].userParams; } else { @@ -5983,7 +6078,7 @@ private static bool ShouldSendParameter(SqlParameter p, bool includeReturnValue } } - private int CountSendableParameters(SqlParameterCollection parameters) + private static int CountSendableParameters(SqlParameterCollection parameters) { int cParams = 0; @@ -6002,7 +6097,7 @@ private int CountSendableParameters(SqlParameterCollection parameters) } // Returns total number of parameters - private int GetParameterCount(SqlParameterCollection parameters) + private static int GetParameterCount(SqlParameterCollection parameters) { return (null != parameters) ? parameters.Count : 0; } @@ -6105,7 +6200,7 @@ private void BuildExecuteSql(CommandBehavior behavior, string commandText, SqlPa if (userParamCount > 0) { - string paramList = BuildParamList(_stateObj.Parser, BatchRPCMode ? parameters : _parameters); + string paramList = BuildParamList(_stateObj.Parser, _batchRPCMode ? parameters : _parameters); sqlParam = rpc.systemParams[1]; sqlParam.SqlDbType = ((paramList.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText; sqlParam.Size = paramList.Length; @@ -6586,48 +6681,35 @@ private void ClearDescribeParameterEncryptionRequests() internal void ClearBatchCommand() { - List<_SqlRPC> rpcList = _RPCList; - if (null != rpcList) - { - rpcList.Clear(); - } - if (null != _parameterCollectionList) - { - _parameterCollectionList.Clear(); - } - - _SqlRPCBatchArray = null; + _RPCList?.Clear(); _currentlyExecutingBatch = 0; } - internal bool BatchRPCMode + internal void SetBatchRPCMode(bool value, int commandCount = 1) { - get - { - return _batchRPCMode; - } - set + _batchRPCMode = value; + ClearBatchCommand(); + if (_batchRPCMode) { - _batchRPCMode = value; - - if (_batchRPCMode == false) + if (_RPCList == null) { - ClearBatchCommand(); + _RPCList = new List<_SqlRPC>(commandCount); } else { - if (_RPCList == null) - { - _RPCList = new List<_SqlRPC>(); - } - if (_parameterCollectionList == null) - { - _parameterCollectionList = new List(); - } + _RPCList.Capacity = commandCount; } } } + internal void SetBatchRPCModeReadyToExecute() + { + Debug.Assert(_batchRPCMode, "Command is not in batch RPC Mode"); + Debug.Assert(_RPCList != null, "No batch commands specified"); + + _currentlyExecutingBatch = 0; + } + /// /// Set the column encryption setting to the new one. /// Do not allow conflicting column encryption settings. @@ -6645,71 +6727,86 @@ private void SetColumnEncryptionSetting(SqlCommandColumnEncryptionSetting newCol } } - internal void AddBatchCommand(string commandText, SqlParameterCollection parameters, CommandType cmdType, SqlCommandColumnEncryptionSetting columnEncryptionSetting) + internal void AddBatchCommand(SqlBatchCommand batchCommand) { - Debug.Assert(BatchRPCMode, "Command is not in batch RPC Mode"); + Debug.Assert(_batchRPCMode, "Command is not in batch RPC Mode"); Debug.Assert(_RPCList != null); - Debug.Assert(_parameterCollectionList != null); - _SqlRPC rpc = new _SqlRPC(); + _SqlRPC rpc = new _SqlRPC + { + batchCommand = batchCommand + }; + string commandText = batchCommand.CommandText; + CommandType cmdType = batchCommand.CommandType; CommandText = commandText; CommandType = cmdType; // Set the column encryption setting. - SetColumnEncryptionSetting(columnEncryptionSetting); + SetColumnEncryptionSetting(batchCommand.ColumnEncryptionSetting); GetStateObject(); if (cmdType == CommandType.StoredProcedure) { - BuildRPC(false, parameters, ref rpc); + BuildRPC(false, batchCommand.Parameters, ref rpc); } else { // All batch sql statements must be executed inside sp_executesql, including those without parameters - BuildExecuteSql(CommandBehavior.Default, commandText, parameters, ref rpc); + BuildExecuteSql(CommandBehavior.Default, commandText, batchCommand.Parameters, ref rpc); } _RPCList.Add(rpc); - // Always add a parameters collection per RPC, even if there are no parameters. - _parameterCollectionList.Add(parameters); ReliablePutStateObject(); } - internal int ExecuteBatchRPCCommand() + internal int? GetRecordsAffected(int commandIndex) { - Debug.Assert(BatchRPCMode, "Command is not in batch RPC Mode"); - Debug.Assert(_RPCList != null, "No batch commands specified"); + Debug.Assert(_batchRPCMode, "Command is not in batch RPC Mode"); + Debug.Assert(_RPCList != null, "batch command have been cleared"); + return _RPCList[commandIndex].recordsAffected; + } - _SqlRPCBatchArray = _RPCList.ToArray(); - _currentlyExecutingBatch = 0; - return ExecuteNonQuery(); // Check permissions, execute, return output params + internal SqlBatchCommand GetCurrentBatchCommand() + { + if (_batchRPCMode) + { + return _RPCList[_currentlyExecutingBatch].batchCommand; + } + else + { + return _rpcArrayOf1[0].batchCommand; + } } - internal int? GetRecordsAffected(int commandIndex) + internal SqlBatchCommand GetBatchCommand(int index) + { + return _RPCList[index].batchCommand; + } + + internal int GetCurrentBatchIndex() { - Debug.Assert(BatchRPCMode, "Command is not in batch RPC Mode"); - Debug.Assert(_SqlRPCBatchArray != null, "batch command have been cleared"); - return _SqlRPCBatchArray[commandIndex].recordsAffected; + return _batchRPCMode ? _currentlyExecutingBatch : -1; } internal SqlException GetErrors(int commandIndex) { SqlException result = null; - int length = (_SqlRPCBatchArray[commandIndex].errorsIndexEnd - _SqlRPCBatchArray[commandIndex].errorsIndexStart); + _SqlRPC rpc = _RPCList[commandIndex]; + int length = (rpc.errorsIndexEnd - rpc.errorsIndexStart); if (0 < length) { SqlErrorCollection errors = new SqlErrorCollection(); - for (int i = _SqlRPCBatchArray[commandIndex].errorsIndexStart; i < _SqlRPCBatchArray[commandIndex].errorsIndexEnd; ++i) + for (int i = rpc.errorsIndexStart; i < rpc.errorsIndexEnd; ++i) { - errors.Add(_SqlRPCBatchArray[commandIndex].errors[i]); + errors.Add(rpc.errors[i]); } - for (int i = _SqlRPCBatchArray[commandIndex].warningsIndexStart; i < _SqlRPCBatchArray[commandIndex].warningsIndexEnd; ++i) + for (int i = rpc.warningsIndexStart; i < rpc.warningsIndexEnd; ++i) { - errors.Add(_SqlRPCBatchArray[commandIndex].warnings[i]); + errors.Add(rpc.warnings[i]); } - result = SqlException.CreateException(errors, Connection.ServerVersion, Connection.ClientConnectionId); + result = SqlException.CreateException(errors, Connection.ServerVersion, Connection.ClientConnectionId, innerException: null, batchCommand: null); } return result; } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlUtil.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlUtil.cs index 1998e13e96..3583310717 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlUtil.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlUtil.cs @@ -828,7 +828,7 @@ static internal Exception CannotCompleteDelegatedTransactionWithOpenResults(SqlI { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(TdsEnums.TIMEOUT_EXPIRED, (byte)0x00, TdsEnums.MIN_ERROR_CLASS, null, (StringsHelper.GetString(Strings.ADP_OpenReaderExists, marsOn ? ADP.Command : ADP.Connection)), "", 0, TdsEnums.SNI_WAIT_TIMEOUT)); - return SqlException.CreateException(errors, null, internalConnection); + return SqlException.CreateException(errors, null, internalConnection, innerException: null, batchCommand: null); } internal static TransactionPromotionException PromotionFailed(Exception inner) @@ -1074,7 +1074,7 @@ internal static Exception MultiSubnetFailoverWithFailoverPartner(bool serverProv // Replacing InvalidOperation with SQL exception SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, (byte)0x00, TdsEnums.FATAL_ERROR_CLASS, null, msg, "", 0)); - SqlException exc = SqlException.CreateException(errors, null, internalConnection); + SqlException exc = SqlException.CreateException(errors, null, internalConnection, innerException: null, batchCommand: null); exc._doNotReconnect = true; // disable open retry logic on this error return exc; } @@ -1115,7 +1115,7 @@ internal static Exception ROR_FailoverNotSupportedServer(SqlInternalConnectionTd { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, (byte)0x00, TdsEnums.FATAL_ERROR_CLASS, null, (StringsHelper.GetString(Strings.SQLROR_FailoverNotSupported)), "", 0)); - SqlException exc = SqlException.CreateException(errors, null, internalConnection); + SqlException exc = SqlException.CreateException(errors, null, internalConnection, innerException: null, batchCommand: null); exc._doNotReconnect = true; return exc; } @@ -1124,7 +1124,7 @@ internal static Exception ROR_RecursiveRoutingNotSupported(SqlInternalConnection { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, (byte)0x00, TdsEnums.FATAL_ERROR_CLASS, null, (StringsHelper.GetString(Strings.SQLROR_RecursiveRoutingNotSupported)), "", 0)); - SqlException exc = SqlException.CreateException(errors, null, internalConnection); + SqlException exc = SqlException.CreateException(errors, null, internalConnection, innerException: null, batchCommand: null); exc._doNotReconnect = true; return exc; } @@ -1133,7 +1133,7 @@ internal static Exception ROR_UnexpectedRoutingInfo(SqlInternalConnectionTds int { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, (byte)0x00, TdsEnums.FATAL_ERROR_CLASS, null, (StringsHelper.GetString(Strings.SQLROR_UnexpectedRoutingInfo)), "", 0)); - SqlException exc = SqlException.CreateException(errors, null, internalConnection); + SqlException exc = SqlException.CreateException(errors, null, internalConnection, innerException: null, batchCommand: null); exc._doNotReconnect = true; return exc; } @@ -1142,7 +1142,7 @@ internal static Exception ROR_InvalidRoutingInfo(SqlInternalConnectionTds intern { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, (byte)0x00, TdsEnums.FATAL_ERROR_CLASS, null, (StringsHelper.GetString(Strings.SQLROR_InvalidRoutingInfo)), "", 0)); - SqlException exc = SqlException.CreateException(errors, null, internalConnection); + SqlException exc = SqlException.CreateException(errors, null, internalConnection, innerException: null, batchCommand: null); exc._doNotReconnect = true; return exc; } @@ -1151,7 +1151,7 @@ internal static Exception ROR_TimeoutAfterRoutingInfo(SqlInternalConnectionTds i { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, (byte)0x00, TdsEnums.FATAL_ERROR_CLASS, null, (StringsHelper.GetString(Strings.SQLROR_TimeoutAfterRoutingInfo)), "", 0)); - SqlException exc = SqlException.CreateException(errors, null, internalConnection); + SqlException exc = SqlException.CreateException(errors, null, internalConnection, innerException: null, batchCommand: null); exc._doNotReconnect = true; return exc; } @@ -1187,7 +1187,7 @@ internal static Exception CR_EncryptionChanged(SqlInternalConnectionTds internal { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, 0, TdsEnums.FATAL_ERROR_CLASS, null, StringsHelper.GetString(Strings.SQLCR_EncryptionChanged), "", 0)); - SqlException exc = SqlException.CreateException(errors, "", internalConnection); + SqlException exc = SqlException.CreateException(errors, "", internalConnection, innerException: null, batchCommand: null); return exc; } @@ -1195,7 +1195,7 @@ internal static SqlException CR_AllAttemptsFailed(SqlException innerException, G { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, 0, TdsEnums.MIN_ERROR_CLASS, null, StringsHelper.GetString(Strings.SQLCR_AllAttemptsFailed), "", 0)); - SqlException exc = SqlException.CreateException(errors, "", connectionId, innerException); + SqlException exc = SqlException.CreateException(errors, "", connectionId, innerException, batchCommand: null); return exc; } @@ -1203,7 +1203,7 @@ internal static SqlException CR_NoCRAckAtReconnection(SqlInternalConnectionTds i { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, 0, TdsEnums.FATAL_ERROR_CLASS, null, StringsHelper.GetString(Strings.SQLCR_NoCRAckAtReconnection), "", 0)); - SqlException exc = SqlException.CreateException(errors, "", internalConnection); + SqlException exc = SqlException.CreateException(errors, "", internalConnection, innerException: null, batchCommand: null); return exc; } @@ -1211,7 +1211,7 @@ internal static SqlException CR_TDSVersionNotPreserved(SqlInternalConnectionTds { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, 0, TdsEnums.FATAL_ERROR_CLASS, null, StringsHelper.GetString(Strings.SQLCR_TDSVestionNotPreserved), "", 0)); - SqlException exc = SqlException.CreateException(errors, "", internalConnection); + SqlException exc = SqlException.CreateException(errors, "", internalConnection, innerException: null, batchCommand: null); return exc; } @@ -1219,7 +1219,7 @@ internal static SqlException CR_UnrecoverableServer(Guid connectionId) { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, 0, TdsEnums.FATAL_ERROR_CLASS, null, StringsHelper.GetString(Strings.SQLCR_UnrecoverableServer), "", 0)); - SqlException exc = SqlException.CreateException(errors, "", connectionId); + SqlException exc = SqlException.CreateException(errors, "", connectionId, innerException: null, batchCommand: null); return exc; } @@ -1227,7 +1227,7 @@ internal static SqlException CR_UnrecoverableClient(Guid connectionId) { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, 0, TdsEnums.FATAL_ERROR_CLASS, null, StringsHelper.GetString(Strings.SQLCR_UnrecoverableClient), "", 0)); - SqlException exc = SqlException.CreateException(errors, "", connectionId); + SqlException exc = SqlException.CreateException(errors, "", connectionId, innerException: null, batchCommand: null); return exc; } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs index b2034acaea..8c6f531c1e 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -1243,7 +1243,7 @@ internal void Disconnect() } // Fires a single InfoMessageEvent - private void FireInfoMessageEvent(SqlConnection connection, TdsParserStateObject stateObj, SqlError error) + private void FireInfoMessageEvent(SqlConnection connection, SqlCommand command, TdsParserStateObject stateObj, SqlError error) { string serverVersion = null; @@ -1258,7 +1258,7 @@ private void FireInfoMessageEvent(SqlConnection connection, TdsParserStateObject sqlErs.Add(error); - SqlException exc = SqlException.CreateException(sqlErs, serverVersion, _connHandler); + SqlException exc = SqlException.CreateException(sqlErs, serverVersion, _connHandler, innerException:null, batchCommand: command?.GetCurrentBatchCommand()); bool notified; connection.OnInfoMessage(new SqlInfoMessageEventArgs(exc), out notified); @@ -1292,7 +1292,7 @@ internal void RollbackOrphanedAPITransactions() } } - internal void ThrowExceptionAndWarning(TdsParserStateObject stateObj, bool callerHasConnectionLock = false, bool asyncClose = false) + internal void ThrowExceptionAndWarning(TdsParserStateObject stateObj, SqlCommand command = null, bool callerHasConnectionLock = false, bool asyncClose = false) { Debug.Assert(!callerHasConnectionLock || _connHandler._parserLock.ThreadMayHaveLock(), "Caller claims to have lock, but connection lock is not taken"); @@ -1343,11 +1343,16 @@ internal void ThrowExceptionAndWarning(TdsParserStateObject stateObj, bool calle if (temp.Count == 1 && temp[0].Exception != null) { - exception = SqlException.CreateException(temp, serverVersion, _connHandler, temp[0].Exception); + exception = SqlException.CreateException(temp, serverVersion, _connHandler, temp[0].Exception, command?.GetBatchCommand(temp[0].BatchIndex)); } else { - exception = SqlException.CreateException(temp, serverVersion, _connHandler); + SqlBatchCommand batchCommand = null; + if (temp[0]?.BatchIndex is var index and >= 0 && command is not null) + { + batchCommand = command.GetBatchCommand(index.Value); + } + exception = SqlException.CreateException(temp, serverVersion, _connHandler, innerException: null, batchCommand: batchCommand); } } @@ -2003,11 +2008,15 @@ internal bool TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataRead if ((connection != null) && connection.FireInfoMessageEventOnUserErrors) { foreach (SqlError error in stateObj._pendingInfoEvents) - FireInfoMessageEvent(connection, stateObj, error); + { + FireInfoMessageEvent(connection, cmdHandler, stateObj, error); + } } else foreach (SqlError error in stateObj._pendingInfoEvents) + { stateObj.AddWarning(error); + } } stateObj._pendingInfoEvents = null; } @@ -2044,7 +2053,7 @@ internal bool TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataRead } SqlError error; - if (!TryProcessError(token, stateObj, out error)) + if (!TryProcessError(token, stateObj, cmdHandler, out error)) { return false; } @@ -2073,7 +2082,7 @@ internal bool TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataRead (error.Class <= TdsEnums.MAX_USER_CORRECTABLE_ERROR_CLASS)) { // Fire SqlInfoMessage here - FireInfoMessageEvent(connection, stateObj, error); + FireInfoMessageEvent(connection, cmdHandler, stateObj, error); } else { @@ -2166,7 +2175,7 @@ internal bool TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataRead } else { - cmdHandler.OnDoneProc(); + cmdHandler.OnDoneProc(stateObj); } } @@ -2610,14 +2619,14 @@ internal bool TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataRead if (RunBehavior.Clean != (RunBehavior.Clean & runBehavior) && !stateObj.IsTimeoutStateExpired) { // Add attention error to collection - if not RunBehavior.Clean! - stateObj.AddError(new SqlError(0, 0, TdsEnums.MIN_ERROR_CLASS, _server, SQLMessage.OperationCancelled(), "", 0)); + stateObj.AddError(new SqlError(0, 0, TdsEnums.MIN_ERROR_CLASS, _server, SQLMessage.OperationCancelled(), "", 0, exception: null, batchIndex: cmdHandler?.GetCurrentBatchIndex() ?? -1)); } } } if (stateObj.HasErrorOrWarning) { - ThrowExceptionAndWarning(stateObj); + ThrowExceptionAndWarning(stateObj, cmdHandler); } return true; } @@ -3064,7 +3073,7 @@ private bool TryProcessDone(SqlCommand cmd, SqlDataReader reader, ref RunBehavio if ((TdsEnums.DONE_ERROR == (TdsEnums.DONE_ERROR & status)) && stateObj.ErrorCount == 0 && stateObj.HasReceivedError == false && (RunBehavior.Clean != (RunBehavior.Clean & run))) { - stateObj.AddError(new SqlError(0, 0, TdsEnums.MIN_ERROR_CLASS, _server, SQLMessage.SevereError(), "", 0)); + stateObj.AddError(new SqlError(0, 0, TdsEnums.MIN_ERROR_CLASS, _server, SQLMessage.SevereError(), "", 0, exception: null, batchIndex: cmd?.GetCurrentBatchIndex() ?? -1)); if (null != reader) { @@ -3080,7 +3089,7 @@ private bool TryProcessDone(SqlCommand cmd, SqlDataReader reader, ref RunBehavio // The server will always break the connection in this case. if ((TdsEnums.DONE_SRVERROR == (TdsEnums.DONE_SRVERROR & status)) && (RunBehavior.Clean != (RunBehavior.Clean & run))) { - stateObj.AddError(new SqlError(0, 0, TdsEnums.FATAL_ERROR_CLASS, _server, SQLMessage.SevereError(), "", 0)); + stateObj.AddError(new SqlError(0, 0, TdsEnums.FATAL_ERROR_CLASS, _server, SQLMessage.SevereError(), "", 0, exception: null, batchIndex: cmd?.GetCurrentBatchIndex() ?? -1)); if (null != reader) { @@ -3883,7 +3892,7 @@ private bool TryProcessFedAuthInfo(TdsParserStateObject stateObj, int tokenLen, return true; } - internal bool TryProcessError(byte token, TdsParserStateObject stateObj, out SqlError error) + internal bool TryProcessError(byte token, TdsParserStateObject stateObj, SqlCommand command, out SqlError error) { ushort shortLen; byte byteLen; @@ -3955,8 +3964,12 @@ internal bool TryProcessError(byte token, TdsParserStateObject stateObj, out Sql { return false; } - - error = new SqlError(number, state, errorClass, _server, message, procedure, line); + int batchIndex = -1; + if (command != null) + { + batchIndex = command.GetCurrentBatchIndex(); + } + error = new SqlError(number, state, errorClass, _server, message, procedure, line,exception: null, batchIndex: batchIndex); return true; } @@ -9066,7 +9079,7 @@ internal Task TdsExecuteSQLBatch(string text, int timeout, SqlNotificationReques } } - internal Task TdsExecuteRPC(SqlCommand cmd, _SqlRPC[] rpcArray, int timeout, bool inSchema, SqlNotificationRequest notificationRequest, TdsParserStateObject stateObj, bool isCommandProc, bool sync = true, + internal Task TdsExecuteRPC(SqlCommand cmd, IList<_SqlRPC> rpcArray, int timeout, bool inSchema, SqlNotificationRequest notificationRequest, TdsParserStateObject stateObj, bool isCommandProc, bool sync = true, TaskCompletionSource completion = null, int startRpc = 0, int startParam = 0) { bool firstCall = (completion == null); @@ -9128,7 +9141,7 @@ internal Task TdsExecuteRPC(SqlCommand cmd, _SqlRPC[] rpcArray, int timeout, boo stateObj._outputMessageType = TdsEnums.MT_RPC; } - for (int ii = startRpc; ii < rpcArray.Length; ii++) + for (int ii = startRpc; ii < rpcArray.Count; ii++) { rpcext = rpcArray[ii]; @@ -9269,7 +9282,7 @@ internal Task TdsExecuteRPC(SqlCommand cmd, _SqlRPC[] rpcArray, int timeout, boo } // parameter for loop // If this is not the last RPC we are sending, add the batch flag - if (ii < (rpcArray.Length - 1)) + if (ii < (rpcArray.Count - 1)) { stateObj.WriteByte(TdsEnums.SQL2005_RPCBATCHFLAG); } @@ -9785,7 +9798,7 @@ private Task TDSExecuteRPCAddParameter(TdsParserStateObject stateObj, SqlParamet } // This is in its own method to avoid always allocating the lambda in TDSExecuteRPCParameter - private void TDSExecuteRPCParameterSetupWriteCompletion(SqlCommand cmd, _SqlRPC[] rpcArray, int timeout, bool inSchema, SqlNotificationRequest notificationRequest, TdsParserStateObject stateObj, bool isCommandProc, bool sync, TaskCompletionSource completion, int startRpc, int startParam, Task writeParamTask) + private void TDSExecuteRPCParameterSetupWriteCompletion(SqlCommand cmd, IList<_SqlRPC> rpcArray, int timeout, bool inSchema, SqlNotificationRequest notificationRequest, TdsParserStateObject stateObj, bool isCommandProc, bool sync, TaskCompletionSource completion, int startRpc, int startParam, Task writeParamTask) { AsyncHelper.ContinueTask( writeParamTask, diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs index be0f437b5b..9a6ceb7054 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs @@ -614,6 +614,8 @@ internal sealed class _SqlRPC internal bool needsFetchParameterEncryptionMetadata; + internal SqlBatchCommand batchCommand; + internal string GetCommandTextOrRpcName() { if (TdsEnums.RPC_PROCID_EXECUTESQL == ProcID) diff --git a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj index 7a80ce32da..19e1e5c7b6 100644 --- a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.csproj @@ -18,4 +18,4 @@ - \ No newline at end of file + diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj index 0012c14892..c81604f3c3 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -362,6 +362,9 @@ Microsoft\Data\SqlClient\SqlAuthenticationToken.cs + + Microsoft\Data\SqlClient\SqlBatchCommand.cs + Microsoft\Data\SqlClient\SqlBulkCopyColumnMapping.cs @@ -738,4 +741,4 @@ - \ No newline at end of file + diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs index 64cdd56249..0ac6ed775e 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlCommand.cs @@ -318,15 +318,13 @@ private CachedAsyncState cachedAsyncState private bool _batchRPCMode; private List<_SqlRPC> _RPCList; - private _SqlRPC[] _SqlRPCBatchArray; private _SqlRPC[] _sqlRPCParameterEncryptionReqArray; - private List _parameterCollectionList; private int _currentlyExecutingBatch; private SqlRetryLogicBaseProvider _retryLogicProvider; /// /// This variable is used to keep track of which RPC batch's results are being read when reading the results of - /// describe parameter encryption RPC requests in BatchRPCMode. + /// describe parameter encryption RPC requests in _batchRPCMode. /// private int _currentlyExecutingDescribeParameterEncryptionRPC; @@ -1385,7 +1383,7 @@ public override object ExecuteScalar() ds = IsProviderRetriable ? RunExecuteReaderWithRetry(0, RunBehavior.ReturnImmediately, true, nameof(ExecuteScalar)) : RunExecuteReader(0, RunBehavior.ReturnImmediately, true, nameof(ExecuteScalar)); - object result = CompleteExecuteScalar(ds, false); + object result = CompleteExecuteScalar(ds, _batchRPCMode); success = true; return result; } @@ -1402,26 +1400,102 @@ public override object ExecuteScalar() } } - private object CompleteExecuteScalar(SqlDataReader ds, bool returnSqlValue) + internal Task ExecuteScalarBatchAsync(CancellationToken cancellationToken) + { + return ExecuteReaderAsync(cancellationToken).ContinueWith((executeTask) => + { + TaskCompletionSource source = new TaskCompletionSource(); + if (executeTask.IsCanceled) + { + source.SetCanceled(); + } + else if (executeTask.IsFaulted) + { + source.SetException(executeTask.Exception.InnerException); + } + else + { + SqlDataReader reader = executeTask.Result; + ExecuteScalarUntilEndAsync(reader, cancellationToken).ContinueWith( + (readTask) => + { + try + { + if (readTask.IsCanceled) + { + reader.Dispose(); + source.SetCanceled(); + } + else if (readTask.IsFaulted) + { + reader.Dispose(); + source.SetException(readTask.Exception.InnerException); + } + else + { + Exception exception = null; + object result = null; + try + { + result = readTask.Result; + } + finally + { + reader.Dispose(); + } + if (exception != null) + { + source.SetException(exception); + } + else + { + source.SetResult(result); + } + } + } + catch (Exception e) + { + // exception thrown by Dispose... + source.SetException(e); + } + }, + TaskScheduler.Default + ); + } + return source.Task; + }, TaskScheduler.Default).Unwrap(); + } + + private async Task ExecuteScalarUntilEndAsync(SqlDataReader reader, CancellationToken cancellationToken) + { + object retval = null; + do + { + if (await reader.ReadAsync(cancellationToken).ConfigureAwait(false) && reader.FieldCount > 0) + { + retval = reader.GetValue(0); // no async untyped value getter, this will work ok as long as the value is in the current packet + } + } + while (_batchRPCMode && !cancellationToken.IsCancellationRequested && await reader.NextResultAsync(cancellationToken).ConfigureAwait(false)); + return retval; + } + + private object CompleteExecuteScalar(SqlDataReader ds, bool returnLastResult) { object retResult = null; try { - if (ds.Read()) + do { - if (ds.FieldCount > 0) + if (ds.Read()) { - if (returnSqlValue) - { - retResult = ds.GetSqlValue(0); - } - else + if (ds.FieldCount > 0) { retResult = ds.GetValue(0); } } - } + } while (returnLastResult && ds.NextResult()); } finally { @@ -1677,7 +1751,7 @@ private void VerifyEndExecuteState(Task completionTask, string endMethod, bool f { _stateObj.Parser.State = TdsParserState.Broken; // We failed to respond to attention, we have to quit! _stateObj.Parser.Connection.BreakConnection(); - _stateObj.Parser.ThrowExceptionAndWarning(_stateObj); + _stateObj.Parser.ThrowExceptionAndWarning(_stateObj, this); } else { @@ -2007,7 +2081,7 @@ private Task InternalExecuteNonQuery(TaskCompletionSource completion, st //Always Encrypted generally operates only on parameterized queries. However enclave based Always encrypted also supports unparameterized queries //We skip this block for enclave based always encrypted so that we can make a call to SQL Server to get the encryption information - else if (!ShouldUseEnclaveBasedWorkflow && !BatchRPCMode && (System.Data.CommandType.Text == this.CommandType) && (0 == GetParameterCount(_parameters))) + else if (!ShouldUseEnclaveBasedWorkflow && !_batchRPCMode && (System.Data.CommandType.Text == this.CommandType) && (0 == GetParameterCount(_parameters))) { Debug.Assert(!sendToPipe, "trying to send non-context command to pipe"); if (null != statistics) @@ -4135,7 +4209,7 @@ private void PrepareForTransparentEncryption(CommandBehavior cmdBehavior, bool r // If we are not in Batch RPC and not already retrying, attempt to fetch the cipher MD for each parameter from the cache. // If this succeeds then return immediately, otherwise just fall back to the full crypto MD discovery. - if (!BatchRPCMode && !inRetry && (this._parameters != null && this._parameters.Count > 0) && SqlQueryMetadataCache.GetInstance().GetQueryMetadataIfExists(this)) + if (!_batchRPCMode && !inRetry && (this._parameters != null && this._parameters.Count > 0) && SqlQueryMetadataCache.GetInstance().GetQueryMetadataIfExists(this)) { usedCache = true; return; @@ -4150,7 +4224,7 @@ private void PrepareForTransparentEncryption(CommandBehavior cmdBehavior, bool r // Flag to indicate if exception is caught during the execution, to govern clean up. bool exceptionCaught = false; - // Used in BatchRPCMode to maintain a map of describe parameter encryption RPC requests (Keys) and their corresponding original RPC requests (Values). + // Used in _batchRPCMode to maintain a map of describe parameter encryption RPC requests (Keys) and their corresponding original RPC requests (Values). ReadOnlyDictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcMap = null; TdsParser bestEffortCleanupTarget = null; @@ -4185,8 +4259,8 @@ private void PrepareForTransparentEncryption(CommandBehavior cmdBehavior, bool r Debug.Assert(fetchInputParameterEncryptionInfoTask == null || async, "Task returned by TryFetchInputParameterEncryptionInfo, when in sync mode, in PrepareForTransparentEncryption."); - Debug.Assert((describeParameterEncryptionRpcOriginalRpcMap != null) == BatchRPCMode, - "describeParameterEncryptionRpcOriginalRpcMap can be non-null if and only if it is in BatchRPCMode."); + Debug.Assert((describeParameterEncryptionRpcOriginalRpcMap != null) == _batchRPCMode, + "describeParameterEncryptionRpcOriginalRpcMap can be non-null if and only if it is in _batchRPCMode."); // If we didn't have parameters, we can fall back to regular code path, by simply returning. if (!describeParameterEncryptionNeeded) @@ -4478,32 +4552,32 @@ private SqlDataReader TryFetchInputParameterEncryptionInfo(int timeout, } } - if (BatchRPCMode) + if (_batchRPCMode) { // Count the rpc requests that need to be transparently encrypted // We simply look for any parameters in a request and add the request to be queried for parameter encryption Dictionary<_SqlRPC, _SqlRPC> describeParameterEncryptionRpcOriginalRpcDictionary = new Dictionary<_SqlRPC, _SqlRPC>(); - for (int i = 0; i < _SqlRPCBatchArray.Length; i++) + for (int i = 0; i < _RPCList.Count; i++) { - // In BatchRPCMode, the actual T-SQL query is in the first parameter and not present as the rpcName, as is the case with non-BatchRPCMode. + // In _batchRPCMode, the actual T-SQL query is in the first parameter and not present as the rpcName, as is the case with non-_batchRPCMode. // So input parameters start at parameters[1]. parameters[0] is the actual T-SQL Statement. rpcName is sp_executesql. - if (_SqlRPCBatchArray[i].systemParams.Length > 1) + if (_RPCList[i].systemParams.Length > 1) { - _SqlRPCBatchArray[i].needsFetchParameterEncryptionMetadata = true; + _RPCList[i].needsFetchParameterEncryptionMetadata = true; // Since we are going to need multiple RPC objects, allocate a new one here for each command in the batch. _SqlRPC rpcDescribeParameterEncryptionRequest = new _SqlRPC(); // Prepare the describe parameter encryption request. - PrepareDescribeParameterEncryptionRequest(_SqlRPCBatchArray[i], ref rpcDescribeParameterEncryptionRequest, i == 0 ? serializedAttestationParameters : null); + PrepareDescribeParameterEncryptionRequest(_RPCList[i], ref rpcDescribeParameterEncryptionRequest, i == 0 ? serializedAttestationParameters : null); Debug.Assert(rpcDescribeParameterEncryptionRequest != null, "rpcDescribeParameterEncryptionRequest should not be null, after call to PrepareDescribeParameterEncryptionRequest."); Debug.Assert(!describeParameterEncryptionRpcOriginalRpcDictionary.ContainsKey(rpcDescribeParameterEncryptionRequest), "There should not already be a key referring to the current rpcDescribeParameterEncryptionRequest, in the dictionary describeParameterEncryptionRpcOriginalRpcDictionary."); // Add the describe parameter encryption RPC request as the key and its corresponding original rpc request to the dictionary. - describeParameterEncryptionRpcOriginalRpcDictionary.Add(rpcDescribeParameterEncryptionRequest, _SqlRPCBatchArray[i]); + describeParameterEncryptionRpcOriginalRpcDictionary.Add(rpcDescribeParameterEncryptionRequest, _RPCList[i]); } } @@ -4523,7 +4597,7 @@ private SqlDataReader TryFetchInputParameterEncryptionInfo(int timeout, describeParameterEncryptionRpcOriginalRpcMap.Keys.CopyTo(_sqlRPCParameterEncryptionReqArray, 0); Debug.Assert(_sqlRPCParameterEncryptionReqArray.Length > 0, "There should be at-least 1 describe parameter encryption rpc request."); - Debug.Assert(_sqlRPCParameterEncryptionReqArray.Length <= _SqlRPCBatchArray.Length, + Debug.Assert(_sqlRPCParameterEncryptionReqArray.Length <= _RPCList.Count, "The number of decribe parameter encryption RPC requests is more than the number of original RPC requests."); } //Always Encrypted generally operates only on parameterized queries. However enclave based Always encrypted also supports unparameterized queries @@ -4610,8 +4684,8 @@ private void PrepareDescribeParameterEncryptionRequest(_SqlRPC originalRpcReques // Prepare @tsql parameter string text; - // In BatchRPCMode, The actual T-SQL query is in the first parameter and not present as the rpcName, as is the case with non-BatchRPCMode. - if (BatchRPCMode) + // In _batchRPCMode, The actual T-SQL query is in the first parameter and not present as the rpcName, as is the case with non-_batchRPCMode. + if (_batchRPCMode) { Debug.Assert(originalRpcRequest.systemParamCount > 0, "originalRpcRequest didn't have at-least 1 parameter in BatchRPCMode, in PrepareDescribeParameterEncryptionRequest."); @@ -4647,9 +4721,9 @@ private void PrepareDescribeParameterEncryptionRequest(_SqlRPC originalRpcReques string parameterList = null; - // In BatchRPCMode, the input parameters start at parameters[1]. parameters[0] is the T-SQL statement. rpcName is sp_executesql. - // And it is already in the format expected out of BuildParamList, which is not the case with Non-BatchRPCMode. - if (BatchRPCMode) + // In _batchRPCMode, the input parameters start at parameters[1]. parameters[0] is the T-SQL statement. rpcName is sp_executesql. + // And it is already in the format expected out of BuildParamList, which is not the case with Non-_batchRPCMode. + if (_batchRPCMode) { if (originalRpcRequest.systemParamCount > 1) { @@ -4739,10 +4813,10 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi SqlTceCipherInfoEntry cipherInfoEntry; Dictionary columnEncryptionKeyTable = new Dictionary(); - Debug.Assert((describeParameterEncryptionRpcOriginalRpcMap != null) == BatchRPCMode, - "describeParameterEncryptionRpcOriginalRpcMap should be non-null if and only if it is BatchRPCMode."); + Debug.Assert((describeParameterEncryptionRpcOriginalRpcMap != null) == _batchRPCMode, + "describeParameterEncryptionRpcOriginalRpcMap should be non-null if and only if it is _batchRPCMode."); - // Indicates the current result set we are reading, used in BatchRPCMode, where we can have more than 1 result set. + // Indicates the current result set we are reading, used in _batchRPCMode, where we can have more than 1 result set. int resultSetSequenceNumber = 0; #if DEBUG @@ -4750,18 +4824,18 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi int rowsAffected = 0; #endif - // A flag that used in BatchRPCMode, to assert the result of lookup in to the dictionary maintaining the map of describe parameter encryption requests + // A flag that used in _batchRPCMode, to assert the result of lookup in to the dictionary maintaining the map of describe parameter encryption requests // and the corresponding original rpc requests. bool lookupDictionaryResult; do { - if (BatchRPCMode) + if (_batchRPCMode) { // If we got more RPC results from the server than what was requested. if (resultSetSequenceNumber >= _sqlRPCParameterEncryptionReqArray.Length) { - Debug.Assert(false, "Server sent back more results than what was expected for describe parameter encryption requests in BatchRPCMode."); + Debug.Assert(false, "Server sent back more results than what was expected for describe parameter encryption requests in _batchRPCMode."); // Ignore the rest of the results from the server, if for whatever reason it sends back more than what we expect. break; } @@ -4876,7 +4950,7 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi } // Find the RPC command that generated this tce request - if (BatchRPCMode) + if (_batchRPCMode) { Debug.Assert(_sqlRPCParameterEncryptionReqArray[resultSetSequenceNumber] != null, "_sqlRPCParameterEncryptionReqArray[resultSetSequenceNumber] should not be null."); @@ -5032,19 +5106,19 @@ private void ReadDescribeEncryptionParameterResults(SqlDataReader ds, ReadOnlyDi } while (ds.NextResult()); // Verify that we received response for each rpc call needs tce - if (BatchRPCMode) + if (_batchRPCMode) { - for (int i = 0; i < _SqlRPCBatchArray.Length; i++) + for (int i = 0; i < _RPCList.Count; i++) { - if (_SqlRPCBatchArray[i].needsFetchParameterEncryptionMetadata) + if (_RPCList[i].needsFetchParameterEncryptionMetadata) { - throw SQL.ProcEncryptionMetadataMissing(_SqlRPCBatchArray[i].rpcName); + throw SQL.ProcEncryptionMetadataMissing(_RPCList[i].rpcName); } } } // If we are not in Batch RPC mode, update the query cache with the encryption MD. - if (!BatchRPCMode && ShouldCacheEncryptionMetadata && (_parameters is not null && _parameters.Count > 0)) + if (!_batchRPCMode && ShouldCacheEncryptionMetadata && (_parameters is not null && _parameters.Count > 0)) { SqlQueryMetadataCache.GetInstance().AddQueryMetadata(this, ignoreQueriesWithReturnValueParams: true); } @@ -5426,13 +5500,13 @@ private SqlDataReader RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavi Debug.Assert(_sqlRPCParameterEncryptionReqArray != null, "RunExecuteReader rpc array not provided for describe parameter encryption request."); writeTask = _stateObj.Parser.TdsExecuteRPC(this, _sqlRPCParameterEncryptionReqArray, timeout, inSchema, this.Notification, _stateObj, CommandType.StoredProcedure == CommandType, sync: !asyncWrite); } - else if (BatchRPCMode) + else if (_batchRPCMode) { Debug.Assert(inSchema == false, "Batch RPC does not support schema only command behavior"); Debug.Assert(!IsPrepared, "Batch RPC should not be prepared!"); Debug.Assert(!IsDirty, "Batch RPC should not be marked as dirty!"); - Debug.Assert(_SqlRPCBatchArray != null, "RunExecuteReader rpc array not provided"); - writeTask = _stateObj.Parser.TdsExecuteRPC(this, _SqlRPCBatchArray, timeout, inSchema, this.Notification, _stateObj, CommandType.StoredProcedure == CommandType, sync: !asyncWrite); + Debug.Assert(_RPCList != null, "RunExecuteReader rpc array not provided"); + writeTask = _stateObj.Parser.TdsExecuteRPC(this, _RPCList, timeout, inSchema, this.Notification, _stateObj, CommandType.StoredProcedure == CommandType, sync: !asyncWrite); } else if ((CommandType.Text == this.CommandType) && (0 == GetParameterCount(_parameters))) { @@ -5954,21 +6028,29 @@ private void ValidateCommand(string method, bool async) // Check to see if the currently set transaction has completed. If so, // null out our local reference. if (null != _transaction && _transaction.Connection == null) + { _transaction = null; + } // throw if the connection is in a transaction but there is no // locally assigned transaction object if (_activeConnection.HasLocalTransactionFromAPI && (null == _transaction)) + { throw ADP.TransactionRequired(method); + } // if we have a transaction, check to ensure that the active // connection property matches the connection associated with // the transaction if (null != _transaction && _activeConnection != _transaction.Connection) + { throw ADP.TransactionConnectionMismatch(); + } if (ADP.IsEmpty(this.CommandText)) + { throw ADP.CommandTextRequired(method); + } // Notification property must be null for pre-2005 connections if ((Notification != null) && !_activeConnection.Is2005OrNewer) @@ -6100,62 +6182,28 @@ private void PutStateObject() } } - /// - /// IMPORTANT NOTE: This is created as a copy of OnDoneProc below for Transparent Column Encryption improvement - /// as there is not much time, to address regressions. Will revisit removing the duplication, when we have time again. - /// internal void OnDoneDescribeParameterEncryptionProc(TdsParserStateObject stateObj) { // called per rpc batch complete - if (BatchRPCMode) - { - // track the records affected for the just completed rpc batch - // _rowsAffected is cumulative for ExecuteNonQuery across all rpc batches - _sqlRPCParameterEncryptionReqArray[_currentlyExecutingDescribeParameterEncryptionRPC].cumulativeRecordsAffected = _rowsAffected; - - _sqlRPCParameterEncryptionReqArray[_currentlyExecutingDescribeParameterEncryptionRPC].recordsAffected = - (((0 < _currentlyExecutingDescribeParameterEncryptionRPC) && (0 <= _rowsAffected)) - ? (_rowsAffected - Math.Max(_sqlRPCParameterEncryptionReqArray[_currentlyExecutingDescribeParameterEncryptionRPC - 1].cumulativeRecordsAffected, 0)) - : _rowsAffected); - - // track the error collection (not available from TdsParser after ExecuteNonQuery) - // and the which errors are associated with the just completed rpc batch - _sqlRPCParameterEncryptionReqArray[_currentlyExecutingDescribeParameterEncryptionRPC].errorsIndexStart = - ((0 < _currentlyExecutingDescribeParameterEncryptionRPC) - ? _sqlRPCParameterEncryptionReqArray[_currentlyExecutingDescribeParameterEncryptionRPC - 1].errorsIndexEnd - : 0); - _sqlRPCParameterEncryptionReqArray[_currentlyExecutingDescribeParameterEncryptionRPC].errorsIndexEnd = stateObj.ErrorCount; - _sqlRPCParameterEncryptionReqArray[_currentlyExecutingDescribeParameterEncryptionRPC].errors = stateObj._errors; - - // track the warning collection (not available from TdsParser after ExecuteNonQuery) - // and the which warnings are associated with the just completed rpc batch - _sqlRPCParameterEncryptionReqArray[_currentlyExecutingDescribeParameterEncryptionRPC].warningsIndexStart = - ((0 < _currentlyExecutingDescribeParameterEncryptionRPC) - ? _sqlRPCParameterEncryptionReqArray[_currentlyExecutingDescribeParameterEncryptionRPC - 1].warningsIndexEnd - : 0); - _sqlRPCParameterEncryptionReqArray[_currentlyExecutingDescribeParameterEncryptionRPC].warningsIndexEnd = stateObj.WarningCount; - _sqlRPCParameterEncryptionReqArray[_currentlyExecutingDescribeParameterEncryptionRPC].warnings = stateObj._warnings; - + if (_batchRPCMode) + { + OnDone(stateObj, _currentlyExecutingDescribeParameterEncryptionRPC, _sqlRPCParameterEncryptionReqArray, _rowsAffected); _currentlyExecutingDescribeParameterEncryptionRPC++; } } - /// - /// IMPORTANT NOTE: There is a copy of this function above in OnDoneDescribeParameterEncryptionProc. - /// Please consider the changes being done in this function for the above function as well. - /// - internal void OnDoneProc() - { + internal void OnDoneProc(TdsParserStateObject stateObject) + { // called per rpc batch complete - if (BatchRPCMode) + if (_batchRPCMode) { - OnDone(_stateObj, _currentlyExecutingBatch, _SqlRPCBatchArray, _rowsAffected); + OnDone(stateObject, _currentlyExecutingBatch, _RPCList, _rowsAffected); _currentlyExecutingBatch++; - Debug.Assert(_parameterCollectionList.Count >= _currentlyExecutingBatch, "OnDoneProc: Too many DONEPROC events"); + Debug.Assert(_RPCList.Count >= _currentlyExecutingBatch, "OnDoneProc: Too many DONEPROC events"); } } - private static void OnDone(TdsParserStateObject stateObj, int index, _SqlRPC[] array, int rowsAffected) + private static void OnDone(TdsParserStateObject stateObj, int index, IList<_SqlRPC> array, int rowsAffected) { _SqlRPC current = array[index]; _SqlRPC previous = (index > 0) ? array[index - 1] : null; @@ -6169,6 +6217,11 @@ private static void OnDone(TdsParserStateObject stateObj, int index, _SqlRPC[] a ? (rowsAffected - Math.Max(previous.cumulativeRecordsAffected, 0)) : rowsAffected); + if (current.batchCommand != null) + { + current.batchCommand.SetRecordAffected(current.recordsAffected.GetValueOrDefault()); + } + // track the error collection (not available from TdsParser after ExecuteNonQuery) // and the which errors are associated with the just completed rpc batch current.errorsIndexStart = previous?.errorsIndexEnd ?? 0; @@ -6196,11 +6249,11 @@ internal void OnReturnStatus(int status) return; SqlParameterCollection parameters = _parameters; - if (BatchRPCMode) + if (_batchRPCMode) { - if (_parameterCollectionList.Count > _currentlyExecutingBatch) + if (_RPCList.Count > _currentlyExecutingBatch) { - parameters = _parameterCollectionList[_currentlyExecutingBatch]; + parameters = _RPCList[_currentlyExecutingBatch].userParams; } else { @@ -6230,7 +6283,7 @@ internal void OnReturnStatus(int status) // If we are not in Batch RPC mode, update the query cache with the encryption MD. // We can do this now that we have distinguished between ReturnValue and ReturnStatus. // Read comment in AddQueryMetadata() for more details. - if (!BatchRPCMode && CachingQueryMetadataPostponed && + if (!_batchRPCMode && CachingQueryMetadataPostponed && ShouldCacheEncryptionMetadata && (_parameters is not null && _parameters.Count > 0)) { SqlQueryMetadataCache.GetInstance().AddQueryMetadata(this, ignoreQueriesWithReturnValueParams: false); @@ -6455,11 +6508,11 @@ internal void OnParameterAvailableSmi(SmiParameterMetaData metaData, ITypedGette private SqlParameterCollection GetCurrentParameterCollection() { - if (BatchRPCMode) + if (_batchRPCMode) { - if (_parameterCollectionList.Count > _currentlyExecutingBatch) + if (_RPCList.Count > _currentlyExecutingBatch) { - return _parameterCollectionList[_currentlyExecutingBatch]; + return _RPCList[_currentlyExecutingBatch].userParams; } else { @@ -6748,7 +6801,7 @@ private int CountSendableParameters(SqlParameterCollection parameters) } // Returns total number of parameters - private int GetParameterCount(SqlParameterCollection parameters) + private static int GetParameterCount(SqlParameterCollection parameters) { return (null != parameters) ? parameters.Count : 0; } @@ -6849,7 +6902,7 @@ private void BuildExecuteSql(CommandBehavior behavior, string commandText, SqlPa if (userParamCount > 0) { - string paramList = BuildParamList(_stateObj.Parser, BatchRPCMode ? parameters : _parameters); + string paramList = BuildParamList(_stateObj.Parser, _batchRPCMode ? parameters : _parameters); sqlParam = rpc.systemParams[1]; sqlParam.SqlDbType = ((paramList.Length << 1) <= TdsEnums.TYPE_SIZE_LIMIT) ? SqlDbType.NVarChar : SqlDbType.NText; sqlParam.Size = paramList.Length; @@ -7323,34 +7376,6 @@ internal int InternalRecordsAffected } } - internal bool BatchRPCMode - { - get - { - return _batchRPCMode; - } - set - { - _batchRPCMode = value; - - if (_batchRPCMode == false) - { - ClearBatchCommand(); - } - else - { - if (_RPCList == null) - { - _RPCList = new List<_SqlRPC>(); - } - if (_parameterCollectionList == null) - { - _parameterCollectionList = new List(); - } - } - } - } - /// /// Clear the state in sqlcommand related to describe parameter encryption RPC requests. /// @@ -7364,16 +7389,32 @@ private void ClearDescribeParameterEncryptionRequests() internal void ClearBatchCommand() { - List<_SqlRPC> rpcList = _RPCList; - if (null != rpcList) - { - rpcList.Clear(); - } - if (null != _parameterCollectionList) + _RPCList?.Clear(); + _currentlyExecutingBatch = 0; + } + + internal void SetBatchRPCMode(bool value, int commandCount = 1) + { + _batchRPCMode = value; + ClearBatchCommand(); + if (_batchRPCMode) { - _parameterCollectionList.Clear(); + if (_RPCList == null) + { + _RPCList = new List<_SqlRPC>(commandCount); + } + else + { + _RPCList.Capacity = commandCount; + } } - _SqlRPCBatchArray = null; + } + + internal void SetBatchRPCModeReadyToExecute() + { + Debug.Assert(_batchRPCMode, "Command is not in batch RPC Mode"); + Debug.Assert(_RPCList != null, "No batch commands specified"); + _currentlyExecutingBatch = 0; } @@ -7397,71 +7438,85 @@ private void SetColumnEncryptionSetting(SqlCommandColumnEncryptionSetting newCol } } - internal void AddBatchCommand(string commandText, SqlParameterCollection parameters, CommandType cmdType, SqlCommandColumnEncryptionSetting columnEncryptionSetting) + internal void AddBatchCommand(SqlBatchCommand batchCommand) { - Debug.Assert(BatchRPCMode, "Command is not in batch RPC Mode"); + Debug.Assert(_batchRPCMode, "Command is not in batch RPC Mode"); Debug.Assert(_RPCList != null); - Debug.Assert(_parameterCollectionList != null); - _SqlRPC rpc = new _SqlRPC(); + _SqlRPC rpc = new _SqlRPC + { + batchCommand = batchCommand + }; + string commandText = batchCommand.CommandText; + CommandType cmdType = batchCommand.CommandType; CommandText = commandText; CommandType = cmdType; // Set the column encryption setting. - SetColumnEncryptionSetting(columnEncryptionSetting); + SetColumnEncryptionSetting(batchCommand.ColumnEncryptionSetting); GetStateObject(); if (cmdType == CommandType.StoredProcedure) { - BuildRPC(false, parameters, ref rpc); + BuildRPC(false, batchCommand.Parameters, ref rpc); } else { // All batch sql statements must be executed inside sp_executesql, including those without parameters - BuildExecuteSql(CommandBehavior.Default, commandText, parameters, ref rpc); + BuildExecuteSql(CommandBehavior.Default, commandText, batchCommand.Parameters, ref rpc); } _RPCList.Add(rpc); - // Always add a parameters collection per RPC, even if there are no parameters. - _parameterCollectionList.Add(parameters); ReliablePutStateObject(); } - internal int ExecuteBatchRPCCommand() + internal int? GetRecordsAffected(int commandIndex) { - Debug.Assert(BatchRPCMode, "Command is not in batch RPC Mode"); - Debug.Assert(_RPCList != null, "No batch commands specified"); + Debug.Assert(_batchRPCMode, "Command is not in batch RPC Mode"); + Debug.Assert(_RPCList != null, "batch command have been cleared"); + return _RPCList[commandIndex].recordsAffected; + } - _SqlRPCBatchArray = _RPCList.ToArray(); - _currentlyExecutingBatch = 0; - return ExecuteNonQuery(); // Check permissions, execute, return output params + internal SqlBatchCommand GetCurrentBatchCommand() + { + if (_batchRPCMode) + { + return _RPCList[_currentlyExecutingBatch].batchCommand; + } + else + { + return _rpcArrayOf1[0].batchCommand; + } } - internal int? GetRecordsAffected(int commandIndex) + internal SqlBatchCommand GetBatchCommand(int index) + { + return _RPCList[index].batchCommand; + } + + internal int GetCurrentBatchIndex() { - Debug.Assert(BatchRPCMode, "Command is not in batch RPC Mode"); - Debug.Assert(_SqlRPCBatchArray != null, "batch command have been cleared"); - return _SqlRPCBatchArray[commandIndex].recordsAffected; + return _batchRPCMode ? _currentlyExecutingBatch : -1; } internal SqlException GetErrors(int commandIndex) { SqlException result = null; - int length = (_SqlRPCBatchArray[commandIndex].errorsIndexEnd - _SqlRPCBatchArray[commandIndex].errorsIndexStart); + int length = (_RPCList[commandIndex].errorsIndexEnd - _RPCList[commandIndex].errorsIndexStart); if (0 < length) { SqlErrorCollection errors = new SqlErrorCollection(); - for (int i = _SqlRPCBatchArray[commandIndex].errorsIndexStart; i < _SqlRPCBatchArray[commandIndex].errorsIndexEnd; ++i) + for (int i = _RPCList[commandIndex].errorsIndexStart; i < _RPCList[commandIndex].errorsIndexEnd; ++i) { - errors.Add(_SqlRPCBatchArray[commandIndex].errors[i]); + errors.Add(_RPCList[commandIndex].errors[i]); } - for (int i = _SqlRPCBatchArray[commandIndex].warningsIndexStart; i < _SqlRPCBatchArray[commandIndex].warningsIndexEnd; ++i) + for (int i = _RPCList[commandIndex].warningsIndexStart; i < _RPCList[commandIndex].warningsIndexEnd; ++i) { - errors.Add(_SqlRPCBatchArray[commandIndex].warnings[i]); + errors.Add(_RPCList[commandIndex].warnings[i]); } - result = SqlException.CreateException(errors, Connection.ServerVersion, Connection.ClientConnectionId); + result = SqlException.CreateException(errors, Connection.ServerVersion, Connection.ClientConnectionId, innerException: null, batchCommand: null); } return result; } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlUtil.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlUtil.cs index aa75618a7d..7b387fe5c0 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlUtil.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlUtil.cs @@ -960,7 +960,7 @@ static internal Exception CannotCompleteDelegatedTransactionWithOpenResults(SqlI { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(TdsEnums.TIMEOUT_EXPIRED, (byte)0x00, TdsEnums.MIN_ERROR_CLASS, null, (StringsHelper.GetString(Strings.ADP_OpenReaderExists, marsOn ? ADP.Command : ADP.Connection)), "", 0, TdsEnums.SNI_WAIT_TIMEOUT)); - return SqlException.CreateException(errors, null, internalConnection); + return SqlException.CreateException(errors, null, internalConnection, innerException: null, batchCommand: null); } static internal SysTx.TransactionPromotionException PromotionFailed(Exception inner) { @@ -2126,7 +2126,7 @@ static internal Exception MultiSubnetFailoverWithFailoverPartner(bool serverProv // VSTFDEVDIV\DevDiv2\179041 - replacing InvalidOperation with SQL exception SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, (byte)0x00, TdsEnums.FATAL_ERROR_CLASS, null, msg, "", 0)); - SqlException exc = SqlException.CreateException(errors, null, internalConnection); + SqlException exc = SqlException.CreateException(errors, null, internalConnection, innerException: null, batchCommand: null); exc._doNotReconnect = true; // disable open retry logic on this error return exc; } @@ -2167,7 +2167,7 @@ static internal Exception ROR_FailoverNotSupportedServer(SqlInternalConnectionTd { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, (byte)0x00, TdsEnums.FATAL_ERROR_CLASS, null, (StringsHelper.GetString(Strings.SQLROR_FailoverNotSupported)), "", 0)); - SqlException exc = SqlException.CreateException(errors, null, internalConnection); + SqlException exc = SqlException.CreateException(errors, null, internalConnection, innerException: null, batchCommand: null); exc._doNotReconnect = true; return exc; } @@ -2176,7 +2176,7 @@ static internal Exception ROR_RecursiveRoutingNotSupported(SqlInternalConnection { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, (byte)0x00, TdsEnums.FATAL_ERROR_CLASS, null, (StringsHelper.GetString(Strings.SQLROR_RecursiveRoutingNotSupported)), "", 0)); - SqlException exc = SqlException.CreateException(errors, null, internalConnection); + SqlException exc = SqlException.CreateException(errors, null, internalConnection, innerException: null, batchCommand: null); exc._doNotReconnect = true; return exc; } @@ -2185,7 +2185,7 @@ static internal Exception ROR_UnexpectedRoutingInfo(SqlInternalConnectionTds int { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, (byte)0x00, TdsEnums.FATAL_ERROR_CLASS, null, (StringsHelper.GetString(Strings.SQLROR_UnexpectedRoutingInfo)), "", 0)); - SqlException exc = SqlException.CreateException(errors, null, internalConnection); + SqlException exc = SqlException.CreateException(errors, null, internalConnection, innerException: null, batchCommand: null); exc._doNotReconnect = true; return exc; } @@ -2194,7 +2194,7 @@ static internal Exception ROR_InvalidRoutingInfo(SqlInternalConnectionTds intern { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, (byte)0x00, TdsEnums.FATAL_ERROR_CLASS, null, (StringsHelper.GetString(Strings.SQLROR_InvalidRoutingInfo)), "", 0)); - SqlException exc = SqlException.CreateException(errors, null, internalConnection); + SqlException exc = SqlException.CreateException(errors, null, internalConnection, innerException: null, batchCommand: null); exc._doNotReconnect = true; return exc; } @@ -2203,7 +2203,7 @@ static internal Exception ROR_TimeoutAfterRoutingInfo(SqlInternalConnectionTds i { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, (byte)0x00, TdsEnums.FATAL_ERROR_CLASS, null, (StringsHelper.GetString(Strings.SQLROR_TimeoutAfterRoutingInfo)), "", 0)); - SqlException exc = SqlException.CreateException(errors, null, internalConnection); + SqlException exc = SqlException.CreateException(errors, null, internalConnection, innerException: null, batchCommand: null); exc._doNotReconnect = true; return exc; } @@ -2231,7 +2231,7 @@ static internal Exception CR_NextAttemptWillExceedQueryTimeout(SqlException inne { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, 0, TdsEnums.MIN_ERROR_CLASS, null, StringsHelper.GetString(Strings.SQLCR_NextAttemptWillExceedQueryTimeout), "", 0)); - SqlException exc = SqlException.CreateException(errors, "", connectionId, innerException); + SqlException exc = SqlException.CreateException(errors, "", connectionId, innerException, batchCommand: null); return exc; } @@ -2239,7 +2239,7 @@ static internal Exception CR_EncryptionChanged(SqlInternalConnectionTds internal { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, 0, TdsEnums.FATAL_ERROR_CLASS, null, StringsHelper.GetString(Strings.SQLCR_EncryptionChanged), "", 0)); - SqlException exc = SqlException.CreateException(errors, "", internalConnection); + SqlException exc = SqlException.CreateException(errors, "", internalConnection, innerException: null, batchCommand: null); return exc; } @@ -2247,7 +2247,7 @@ static internal SqlException CR_AllAttemptsFailed(SqlException innerException, G { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, 0, TdsEnums.MIN_ERROR_CLASS, null, StringsHelper.GetString(Strings.SQLCR_AllAttemptsFailed), "", 0)); - SqlException exc = SqlException.CreateException(errors, "", connectionId, innerException); + SqlException exc = SqlException.CreateException(errors, "", connectionId, innerException, batchCommand: null); return exc; } @@ -2255,7 +2255,7 @@ static internal SqlException CR_NoCRAckAtReconnection(SqlInternalConnectionTds i { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, 0, TdsEnums.FATAL_ERROR_CLASS, null, StringsHelper.GetString(Strings.SQLCR_NoCRAckAtReconnection), "", 0)); - SqlException exc = SqlException.CreateException(errors, "", internalConnection); + SqlException exc = SqlException.CreateException(errors, "", internalConnection, innerException: null, batchCommand: null); return exc; } @@ -2263,7 +2263,7 @@ static internal SqlException CR_TDSVersionNotPreserved(SqlInternalConnectionTds { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, 0, TdsEnums.FATAL_ERROR_CLASS, null, StringsHelper.GetString(Strings.SQLCR_TDSVestionNotPreserved), "", 0)); - SqlException exc = SqlException.CreateException(errors, "", internalConnection); + SqlException exc = SqlException.CreateException(errors, "", internalConnection, innerException: null, batchCommand: null); return exc; } @@ -2271,7 +2271,7 @@ static internal SqlException CR_UnrecoverableServer(Guid connectionId) { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, 0, TdsEnums.FATAL_ERROR_CLASS, null, StringsHelper.GetString(Strings.SQLCR_UnrecoverableServer), "", 0)); - SqlException exc = SqlException.CreateException(errors, "", connectionId); + SqlException exc = SqlException.CreateException(errors, "", connectionId, innerException: null, batchCommand: null); return exc; } @@ -2279,7 +2279,7 @@ static internal SqlException CR_UnrecoverableClient(Guid connectionId) { SqlErrorCollection errors = new SqlErrorCollection(); errors.Add(new SqlError(0, 0, TdsEnums.FATAL_ERROR_CLASS, null, StringsHelper.GetString(Strings.SQLCR_UnrecoverableClient), "", 0)); - SqlException exc = SqlException.CreateException(errors, "", connectionId); + SqlException exc = SqlException.CreateException(errors, "", connectionId, innerException: null, batchCommand: null); return exc; } internal static Exception Azure_ManagedIdentityException(string msg) diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs index 523a88b480..f4186c947f 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParser.cs @@ -1677,7 +1677,7 @@ internal void Disconnect() } // Fires a single InfoMessageEvent - private void FireInfoMessageEvent(SqlConnection connection, TdsParserStateObject stateObj, SqlError error) + private void FireInfoMessageEvent(SqlConnection connection, SqlCommand command, TdsParserStateObject stateObj, SqlError error) { string serverVersion = null; @@ -1693,7 +1693,7 @@ private void FireInfoMessageEvent(SqlConnection connection, TdsParserStateObject sqlErs.Add(error); - SqlException exc = SqlException.CreateException(sqlErs, serverVersion, _connHandler); + SqlException exc = SqlException.CreateException(sqlErs, serverVersion, _connHandler, innerException: null, batchCommand: command?.GetCurrentBatchCommand()); bool notified; connection.OnInfoMessage(new SqlInfoMessageEventArgs(exc), out notified); @@ -1727,7 +1727,7 @@ internal void RollbackOrphanedAPITransactions() } } - internal void ThrowExceptionAndWarning(TdsParserStateObject stateObj, bool callerHasConnectionLock = false, bool asyncClose = false) + internal void ThrowExceptionAndWarning(TdsParserStateObject stateObj, SqlCommand command = null, bool callerHasConnectionLock = false, bool asyncClose = false) { Debug.Assert(!callerHasConnectionLock || _connHandler._parserLock.ThreadMayHaveLock(), "Caller claims to have lock, but connection lock is not taken"); @@ -1777,10 +1777,19 @@ internal void ThrowExceptionAndWarning(TdsParserStateObject stateObj, bool calle { serverVersion = _connHandler.ServerVersion; } - exception = SqlException.CreateException(temp, serverVersion, _connHandler); - if (exception.Procedure == TdsEnums.INIT_SSPI_PACKAGE) + + if (temp.Count == 1 && temp[0].Exception != null) + { + exception = SqlException.CreateException(temp, serverVersion, _connHandler, temp[0].Exception, command?.GetBatchCommand(temp[0].BatchIndex)); + } + else { - exception._doNotReconnect = true; + SqlBatchCommand batchCommand = null; + if (temp[0]?.BatchIndex is var index and >= 0 && command is not null) + { + batchCommand = command.GetBatchCommand(index.Value); + } + exception = SqlException.CreateException(temp, serverVersion, _connHandler, innerException: null, batchCommand: batchCommand); } } @@ -2442,11 +2451,17 @@ internal bool TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataRead if ((connection != null) && connection.FireInfoMessageEventOnUserErrors) { foreach (SqlError error in stateObj._pendingInfoEvents) - FireInfoMessageEvent(connection, stateObj, error); + { + FireInfoMessageEvent(connection, cmdHandler, stateObj, error); + } } else + { foreach (SqlError error in stateObj._pendingInfoEvents) + { stateObj.AddWarning(error); + } + } } stateObj._pendingInfoEvents = null; @@ -2484,7 +2499,7 @@ internal bool TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataRead } SqlError error; - if (!TryProcessError(token, stateObj, out error)) + if (!TryProcessError(token, stateObj, cmdHandler, out error)) { return false; } @@ -2513,7 +2528,7 @@ internal bool TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataRead (error.Class <= TdsEnums.MAX_USER_CORRECTABLE_ERROR_CLASS)) { // Fire SqlInfoMessage here - FireInfoMessageEvent(connection, stateObj, error); + FireInfoMessageEvent(connection,cmdHandler, stateObj, error); } else { @@ -2607,7 +2622,7 @@ internal bool TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataRead } else { - cmdHandler.OnDoneProc(); + cmdHandler.OnDoneProc(stateObj); } } @@ -3058,14 +3073,14 @@ internal bool TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataRead if (RunBehavior.Clean != (RunBehavior.Clean & runBehavior) && !stateObj.IsTimeoutStateExpired) { // Add attention error to collection - if not RunBehavior.Clean! - stateObj.AddError(new SqlError(0, 0, TdsEnums.MIN_ERROR_CLASS, _server, SQLMessage.OperationCancelled(), "", 0)); + stateObj.AddError(new SqlError(0, 0, TdsEnums.MIN_ERROR_CLASS, _server, SQLMessage.OperationCancelled(), "", 0, exception: null, batchIndex: cmdHandler?.GetCurrentBatchIndex() ?? -1)); } } } if (stateObj.HasErrorOrWarning) { - ThrowExceptionAndWarning(stateObj); + ThrowExceptionAndWarning(stateObj, cmdHandler); } return true; } @@ -3555,7 +3570,7 @@ private bool TryProcessDone(SqlCommand cmd, SqlDataReader reader, ref RunBehavio if ((TdsEnums.DONE_ERROR == (TdsEnums.DONE_ERROR & status)) && stateObj.ErrorCount == 0 && stateObj.HasReceivedError == false && (RunBehavior.Clean != (RunBehavior.Clean & run))) { - stateObj.AddError(new SqlError(0, 0, TdsEnums.MIN_ERROR_CLASS, _server, SQLMessage.SevereError(), "", 0)); + stateObj.AddError(new SqlError(0, 0, TdsEnums.MIN_ERROR_CLASS, _server, SQLMessage.SevereError(), "", 0, exception: null, batchIndex: cmd?.GetCurrentBatchIndex() ?? -1)); if (null != reader) { // SQL BU DT 269516 @@ -3571,7 +3586,7 @@ private bool TryProcessDone(SqlCommand cmd, SqlDataReader reader, ref RunBehavio // MDAC #93896. Also, per Ashwin, the server will always break the connection in this case. if ((TdsEnums.DONE_SRVERROR == (TdsEnums.DONE_SRVERROR & status)) && (RunBehavior.Clean != (RunBehavior.Clean & run))) { - stateObj.AddError(new SqlError(0, 0, TdsEnums.FATAL_ERROR_CLASS, _server, SQLMessage.SevereError(), "", 0)); + stateObj.AddError(new SqlError(0, 0, TdsEnums.FATAL_ERROR_CLASS, _server, SQLMessage.SevereError(), "", 0, exception: null, batchIndex: cmd?.GetCurrentBatchIndex() ?? -1)); if (null != reader) { // SQL BU DT 269516 @@ -4386,7 +4401,7 @@ private bool TryProcessFedAuthInfo(TdsParserStateObject stateObj, int tokenLen, return true; } - internal bool TryProcessError(byte token, TdsParserStateObject stateObj, out SqlError error) + internal bool TryProcessError(byte token, TdsParserStateObject stateObj, SqlCommand command, out SqlError error) { ushort shortLen; byte byteLen; @@ -4492,8 +4507,12 @@ internal bool TryProcessError(byte token, TdsParserStateObject stateObj, out Sql } } } - - error = new SqlError(number, state, errorClass, _server, message, procedure, line); + int batchIndex = -1; + if (command != null) + { + batchIndex = command.GetCurrentBatchIndex(); + } + error = new SqlError(number, state, errorClass, _server, message, procedure, line, exception: null, batchIndex: batchIndex); return true; } @@ -9966,7 +9985,7 @@ internal Task TdsExecuteSQLBatch(string text, int timeout, SqlNotificationReques } } - internal Task TdsExecuteRPC(SqlCommand cmd, _SqlRPC[] rpcArray, int timeout, bool inSchema, SqlNotificationRequest notificationRequest, TdsParserStateObject stateObj, bool isCommandProc, bool sync = true, + internal Task TdsExecuteRPC(SqlCommand cmd, IList<_SqlRPC> rpcArray, int timeout, bool inSchema, SqlNotificationRequest notificationRequest, TdsParserStateObject stateObj, bool isCommandProc, bool sync = true, TaskCompletionSource completion = null, int startRpc = 0, int startParam = 0) { bool firstCall = (completion == null); @@ -10030,7 +10049,7 @@ internal Task TdsExecuteRPC(SqlCommand cmd, _SqlRPC[] rpcArray, int timeout, boo stateObj._outputMessageType = TdsEnums.MT_RPC; } - for (int ii = startRpc; ii < rpcArray.Length; ii++) + for (int ii = startRpc; ii < rpcArray.Count; ii++) { rpcext = rpcArray[ii]; @@ -10616,7 +10635,7 @@ internal Task TdsExecuteRPC(SqlCommand cmd, _SqlRPC[] rpcArray, int timeout, boo } // parameter for loop // If this is not the last RPC we are sending, add the batch flag - if (ii < (rpcArray.Length - 1)) + if (ii < (rpcArray.Count - 1)) { if (_is2005) { diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs index 62f7f59b91..079787506a 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/TdsParserHelperClasses.cs @@ -1181,6 +1181,8 @@ sealed internal class _SqlRPC internal SqlErrorCollection warnings; internal bool needsFetchParameterEncryptionMetadata; + internal SqlBatchCommand batchCommand; + internal string GetCommandTextOrRpcName() { if (TdsEnums.RPC_PROCID_EXECUTESQL == ProcID) diff --git a/src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.Batch.NetCoreApp.cs b/src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.Batch.NetCoreApp.cs new file mode 100644 index 0000000000..6ff4060093 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.Batch.NetCoreApp.cs @@ -0,0 +1,9 @@ +namespace System.Data.Common +{ + /// + public partial class SqlBatchCommand + { + /// + public Microsoft.Data.SqlClient.SqlCommandColumnEncryptionSetting ColumnEncryptionSetting { get { throw null; } set { } } + } +} diff --git a/src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.Batch.cs b/src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.Batch.cs new file mode 100644 index 0000000000..be9b716150 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/ref/Microsoft.Data.SqlClient.Batch.cs @@ -0,0 +1,127 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Data.SqlClient +{ + /// + public class SqlBatch : System.Data.Common.DbBatch + { + /// + public SqlBatch() { throw null; } + /// + public SqlBatch(Microsoft.Data.SqlClient.SqlConnection connection, Microsoft.Data.SqlClient.SqlTransaction transaction = null) { throw null; } + /// + public override int Timeout { get => throw null; set { } } + /// + public new Microsoft.Data.SqlClient.SqlConnection Connection { get => throw null; set { } } + /// + public new Microsoft.Data.SqlClient.SqlTransaction Transaction { get => throw null; set { } } + /// + public new SqlBatchCommandCollection BatchCommands { get => throw null; } + /// + protected override System.Data.Common.DbBatchCommandCollection DbBatchCommands { get => throw null; } + /// + protected override System.Data.Common.DbConnection DbConnection { get => throw null; set { } } + /// + protected override System.Data.Common.DbTransaction DbTransaction { get => throw null; set { } } + /// + public override void Cancel() => throw null; + /// + public override int ExecuteNonQuery() => throw null; + /// + public override System.Threading.Tasks.Task ExecuteNonQueryAsync(System.Threading.CancellationToken cancellationToken = default) => throw null; + /// + public override object ExecuteScalar() => throw null; + /// + public override System.Threading.Tasks.Task ExecuteScalarAsync(System.Threading.CancellationToken cancellationToken = default) => throw null; + /// + public override void Prepare() => throw null; + /// + public override System.Threading.Tasks.Task PrepareAsync(System.Threading.CancellationToken cancellationToken = default) => throw null; + /// + protected override System.Data.Common.DbDataReader ExecuteDbDataReader(System.Data.CommandBehavior behavior) => throw null; + /// + protected override System.Threading.Tasks.Task ExecuteDbDataReaderAsync(System.Data.CommandBehavior behavior, System.Threading.CancellationToken cancellationToken) => throw null; + /// + public System.Collections.Generic.List Commands { get { throw null; } } + /// + public Microsoft.Data.SqlClient.SqlDataReader ExecuteReader() => throw null; + /// + protected override System.Data.Common.DbBatchCommand CreateDbBatchCommand() => throw null; + } + /// + public partial class SqlBatchCommand : System.Data.Common.DbBatchCommand + { + /// + public SqlBatchCommand() => throw null; + /// + public SqlBatchCommand(string commandText, System.Data.CommandType commandType = System.Data.CommandType.Text, System.Collections.Generic.IEnumerable parameters = null, Microsoft.Data.SqlClient.SqlCommandColumnEncryptionSetting columnEncryptionSetting = Microsoft.Data.SqlClient.SqlCommandColumnEncryptionSetting.UseConnectionSetting) { throw null; } + /// + public new Microsoft.Data.SqlClient.SqlParameterCollection Parameters { get { throw null; } } + /// + public override string CommandText { get { throw null; } set { } } + /// + public override System.Data.CommandType CommandType { get { throw null; } set { } } + /// + public System.Data.CommandBehavior CommandBehavior { get { throw null; } set { } } + /// + public override int RecordsAffected { get { throw null; } } + /// + protected override System.Data.Common.DbParameterCollection DbParameterCollection => throw null; + } + /// + public class SqlBatchCommandCollection : System.Data.Common.DbBatchCommandCollection, System.Collections.Generic.IList + { + /// + public override int Count => throw null; + /// + public override bool IsReadOnly => throw null; + /// + System.Collections.Generic.IEnumerator System.Collections.Generic.IEnumerable.GetEnumerator() => throw null; + /// + public override System.Collections.Generic.IEnumerator GetEnumerator() => throw null; + /// + public void Add(Microsoft.Data.SqlClient.SqlBatchCommand item) => throw null; + /// + public override void Add(System.Data.Common.DbBatchCommand item) => throw null; + /// + public override void Clear() => throw null; + /// + public bool Contains(Microsoft.Data.SqlClient.SqlBatchCommand item) => throw null; + /// + public override bool Contains(System.Data.Common.DbBatchCommand item) => throw null; + /// + public void CopyTo(Microsoft.Data.SqlClient.SqlBatchCommand[] array, int arrayIndex) => throw null; + /// + public override void CopyTo(System.Data.Common.DbBatchCommand[] array, int arrayIndex) => throw null; + /// + public int IndexOf(Microsoft.Data.SqlClient.SqlBatchCommand item) => throw null; + /// + public override int IndexOf(System.Data.Common.DbBatchCommand item) => throw null; + /// + public void Insert(int index, Microsoft.Data.SqlClient.SqlBatchCommand item) => throw null; + /// + public override void Insert(int index, System.Data.Common.DbBatchCommand item) => throw null; + /// + public bool Remove(Microsoft.Data.SqlClient.SqlBatchCommand item) => throw null; + /// + public override bool Remove(System.Data.Common.DbBatchCommand item) => throw null; + /// + public override void RemoveAt(int index) => throw null; + Microsoft.Data.SqlClient.SqlBatchCommand System.Collections.Generic.IList.this[int index] { get => throw null; set { } } + /// + public new Microsoft.Data.SqlClient.SqlBatchCommand this[int index] { get => throw null; set { } } + System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => throw null; + /// + protected override System.Data.Common.DbBatchCommand GetBatchCommand(int index) => throw null; + /// + protected override void SetBatchCommand(int index, System.Data.Common.DbBatchCommand batchCommand) => throw null; + } + /// + public sealed partial class SqlException + { + /// + public new Microsoft.Data.SqlClient.SqlBatchCommand BatchCommand { get { throw null; } } + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs index 6665a2cde6..8d16876824 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs @@ -448,7 +448,7 @@ internal static Exception CreateSqlException(MsalException msalException, SqlCon connectionOptions.DataSource, msalException.Message, ActiveDirectoryAuthentication.MSALGetAccessTokenFunctionName, 0)); } - return SqlException.CreateException(sqlErs, "", sender); + return SqlException.CreateException(sqlErs, "", sender, innerException: null, batchCommand: null); } #endregion diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBatch.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBatch.cs new file mode 100644 index 0000000000..ff12f3a572 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBatch.cs @@ -0,0 +1,242 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +#if NET6_0_OR_GREATER + +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Data.Common; + +namespace Microsoft.Data.SqlClient +{ + /// + public class SqlBatch : DbBatch + { + private SqlCommand _batchCommand; + private List _commands; + private SqlBatchCommandCollection _providerCommands; + + /// + public SqlBatch() + { + _batchCommand = new SqlCommand(); + } + /// + public SqlBatch(SqlConnection connection = null, SqlTransaction transaction = null) + : this() + { + Connection = connection; + Transaction = transaction; + } + /// + public override int Timeout + { + get + { + CheckDisposed(); + return _batchCommand.CommandTimeout; + } + set + { + CheckDisposed(); + _batchCommand.CommandTimeout = value; + } + } + /// + protected override DbBatchCommandCollection DbBatchCommands => BatchCommands; + /// + public new SqlBatchCommandCollection BatchCommands => _providerCommands != null ? _providerCommands : _providerCommands = new SqlBatchCommandCollection(Commands); // Commands call will check disposed + /// + protected override DbConnection DbConnection + { + get + { + CheckDisposed(); + return Connection; + } + set + { + CheckDisposed(); + Connection = (SqlConnection)value; + } + } + /// + protected override DbTransaction DbTransaction + { + get + { + CheckDisposed(); + return Transaction; + } + set + { + CheckDisposed(); + Transaction = (SqlTransaction)value; + } + } + + /// + public override void Cancel() + { + CheckDisposed(); + _batchCommand.Cancel(); + } + /// + public override int ExecuteNonQuery() + { + CheckDisposed(); + SetupBatchCommandExecute(); + return _batchCommand.ExecuteNonQuery(); + } + /// + public override Task ExecuteNonQueryAsync(CancellationToken cancellationToken = default) + { + CheckDisposed(); + SetupBatchCommandExecute(); + return _batchCommand.ExecuteNonQueryAsync(cancellationToken); + } + /// + public override object ExecuteScalar() + { + CheckDisposed(); + SetupBatchCommandExecute(); + return _batchCommand.ExecuteScalar(); + } + /// + public override Task ExecuteScalarAsync(CancellationToken cancellationToken = default) + { + CheckDisposed(); + SetupBatchCommandExecute(); + return _batchCommand.ExecuteScalarBatchAsync(cancellationToken); + } + /// + public override void Prepare() + { + CheckDisposed(); + } + /// + public override Task PrepareAsync(CancellationToken cancellationToken = default) + { + CheckDisposed(); + return Task.CompletedTask; + } + /// + public override void Dispose() + { + _batchCommand?.Dispose(); + _batchCommand = null; + _commands?.Clear(); + _commands = null; + base.Dispose(); + } + /// + public List Commands + { + get + { + CheckDisposed(); + return _commands != null ? _commands : _commands = new List(); + } + } + /// + public new SqlConnection Connection + { + get + { + CheckDisposed(); + return _batchCommand.Connection; + } + set + { + CheckDisposed(); + _batchCommand.Connection = value; + } + } + /// + public new SqlTransaction Transaction + { + get + { + CheckDisposed(); + return _batchCommand.Transaction; + } + set + { + CheckDisposed(); + _batchCommand.Transaction = value; + } + } + /// + public SqlDataReader ExecuteReader() + { + CheckDisposed(); + SetupBatchCommandExecute(); + return _batchCommand.ExecuteReader(); + } + /// + public new Task ExecuteReaderAsync(CancellationToken cancellationToken = default) + { + CheckDisposed(); + SetupBatchCommandExecute(); + return _batchCommand.ExecuteReaderAsync(cancellationToken); + } + /// + protected override DbDataReader ExecuteDbDataReader(CommandBehavior behavior) => ExecuteReader(); + /// + protected override Task ExecuteDbDataReaderAsync(CommandBehavior behavior, CancellationToken cancellationToken) + { + CheckDisposed(); + SetupBatchCommandExecute(); + return _batchCommand.ExecuteReaderAsync(cancellationToken) + .ContinueWith((result) => + { + if (result.IsFaulted) + { + throw result.Exception.InnerException; + } + return result.Result; + }, + CancellationToken.None, + TaskContinuationOptions.ExecuteSynchronously | TaskContinuationOptions.NotOnCanceled, + TaskScheduler.Default + ); + } + + private void CheckDisposed() + { + if (_batchCommand is null) + { + throw ADP.ObjectDisposed(this); + } + } + + private void SetupBatchCommandExecute() + { + SqlConnection connection = Connection; + if (connection is null) + { + throw ADP.ConnectionRequired(nameof(SetupBatchCommandExecute)); + } + _batchCommand.Connection = Connection; + _batchCommand.Transaction = Transaction; + _batchCommand.SetBatchRPCMode(true, _commands.Count); + _batchCommand.Parameters.Clear(); + for (int index = 0; index < _commands.Count; index++) + { + _batchCommand.AddBatchCommand(_commands[index]); + } + _batchCommand.SetBatchRPCModeReadyToExecute(); + } + /// + protected override DbBatchCommand CreateDbBatchCommand() + { + return new SqlBatchCommand(); + } + } +} + +#endif diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBatchCommand.Net8OrGreater.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBatchCommand.Net8OrGreater.cs new file mode 100644 index 0000000000..d31480afc7 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBatchCommand.Net8OrGreater.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Data.Common; + +namespace Microsoft.Data.SqlClient +{ + public partial class SqlBatchCommand + { + /// + public override DbParameter CreateParameter() => new SqlParameter(); + + /// + public override bool CanCreateParameter => true; + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBatchCommand.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBatchCommand.cs new file mode 100644 index 0000000000..b9cf7296b5 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBatchCommand.cs @@ -0,0 +1,146 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Data; +using System.Data.Common; +using Microsoft.Data.Common; + +namespace Microsoft.Data.SqlClient +{ + /// + public partial class SqlBatchCommand +#if NET6_0_OR_GREATER + : DbBatchCommand +#endif + { + private string _text; + private CommandType _type; + private SqlParameterCollection _parameters; + private CommandBehavior _behavior; + private int _recordsAffected; + private SqlCommandColumnEncryptionSetting _encryptionSetting; + + /// + public SqlBatchCommand() + { + _type = CommandType.Text; + } + /// + public SqlBatchCommand(string commandText, CommandType commandType = CommandType.Text, IEnumerable parameters = null, SqlCommandColumnEncryptionSetting columnEncryptionSetting = SqlCommandColumnEncryptionSetting.UseConnectionSetting) + { + if (string.IsNullOrEmpty(commandText)) + { + throw ADP.CommandTextRequired(nameof(SqlBatchCommand)); + } + _text = commandText; + SetCommandType(commandType); + if (parameters != null) + { + SqlParameterCollection parameterCollection = null; + if (parameters is IList list) + { + parameterCollection = new SqlParameterCollection(list.Count); + for (int index = 0; index < list.Count; index++) + { + parameterCollection.Add(list[index]); + } + } + else + { + parameterCollection = new SqlParameterCollection(); + foreach (SqlParameter parameter in parameters) + { + parameterCollection.Add(parameter); + } + } + _parameters = parameterCollection; + } + _encryptionSetting = columnEncryptionSetting; + } + + // parameter order is reversed for this internal method to avoid ambiguous call sites with the public constructor + // this overload is used internally to take the parameters passed instead of copying them + internal SqlBatchCommand(string commandText, SqlParameterCollection parameterCollection, CommandType commandType, SqlCommandColumnEncryptionSetting columnEncryptionSetting) + : this(commandText, commandType, null, columnEncryptionSetting) + { + _parameters = parameterCollection; + } + + /// + public +#if NET6_0_OR_GREATER + override +#endif + string CommandText { get => _text; set => _text = value; } + + /// + public +#if NET6_0_OR_GREATER + override +#endif + CommandType CommandType { get => _type; set => SetCommandType(value); } + /// + public CommandBehavior CommandBehavior { get => _behavior; set => _behavior = value; } + + /// + public +#if NET6_0_OR_GREATER + override +#endif + int RecordsAffected { get => _recordsAffected; } + + /// + protected +#if NET6_0_OR_GREATER + override +#endif + DbParameterCollection DbParameterCollection => Parameters; + /// + public +#if NET6_0_OR_GREATER + new +#endif + SqlParameterCollection Parameters + { + get + { + if (_parameters is null) + { + _parameters = new SqlParameterCollection(); + } + return _parameters; + } + internal set + { + _parameters = value; + } + } + /// + public SqlCommandColumnEncryptionSetting ColumnEncryptionSetting { get => _encryptionSetting; set => _encryptionSetting = value; } + + private void SetCommandType(CommandType value) + { + if (value != _type) + { + switch (value) + { + case CommandType.Text: + case CommandType.StoredProcedure: + _type = value; + break; + case System.Data.CommandType.TableDirect: + throw SQL.NotSupportedCommandType(value); + default: + throw ADP.InvalidCommandType(value); + } + } + } + + internal void SetRecordAffected(int value) + { + _recordsAffected = value; + } + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBatchCommandCollection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBatchCommandCollection.cs new file mode 100644 index 0000000000..e824c2f414 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBatchCommandCollection.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. +#if NET6_0_OR_GREATER + +using System.Collections.Generic; +using System.Data.Common; +using Microsoft.Data.Common; + +namespace Microsoft.Data.SqlClient +{ + /// + public class SqlBatchCommandCollection : DbBatchCommandCollection, IList + { + readonly List _list; + + internal SqlBatchCommandCollection(List batchCommands) + { + _list = batchCommands; + } + /// + public override int Count => _list.Count; + /// + public override bool IsReadOnly => false; + /// + IEnumerator IEnumerable.GetEnumerator() => _list.GetEnumerator(); + /// + public override IEnumerator GetEnumerator() => _list.GetEnumerator(); + /// + public void Add(SqlBatchCommand item) => _list.Add(item); + /// + public override void Add(DbBatchCommand item) => Add((SqlBatchCommand)item); + /// + public override void Clear() => _list.Clear(); + /// + public bool Contains(SqlBatchCommand item) => _list.Contains(item); + /// + public override bool Contains(DbBatchCommand item) => Contains((SqlBatchCommand)item); + /// + public void CopyTo(SqlBatchCommand[] array, int arrayIndex) => _list.CopyTo(array, arrayIndex); + /// + public override void CopyTo(DbBatchCommand[] array, int arrayIndex) + { + SqlBatchCommand[] target = (SqlBatchCommand[])array; + CopyTo(target, arrayIndex); + } + /// + public int IndexOf(SqlBatchCommand item) => _list.IndexOf(item); + /// + public override int IndexOf(DbBatchCommand item) => IndexOf((SqlBatchCommand)item); + /// + public void Insert(int index, SqlBatchCommand item) => _list.Insert(index, item); + /// + public override void Insert(int index, DbBatchCommand item) => Insert(index, (SqlBatchCommand)item); + /// + public bool Remove(SqlBatchCommand item) => _list.Remove(item); + /// + public override bool Remove(DbBatchCommand item) => Remove((SqlBatchCommand)item); + /// + public override void RemoveAt(int index) => _list.RemoveAt(index); + /// + SqlBatchCommand IList.this[int index] + { + get => _list[index]; + set => _list[index] = value; + } + /// + public new SqlBatchCommand this[int index] + { + get => _list[index]; + set => _list[index] = value; + } + /// + protected override DbBatchCommand GetBatchCommand(int index) => _list[index]; + + /// + protected override void SetBatchCommand(int index, DbBatchCommand batchCommand) + => _list[index] = (SqlBatchCommand)batchCommand; + + } +} + +#endif diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientEventSource.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientEventSource.cs index 3690975f73..c82f4d625a 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientEventSource.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientEventSource.cs @@ -443,6 +443,15 @@ internal void TryTraceEvent(string message, T0 args0, T1 Trace(string.Format(message, args0?.ToString() ?? NullStr, args1?.ToString() ?? NullStr, args2?.ToString() ?? NullStr, args3?.ToString() ?? NullStr, args4?.ToString() ?? NullStr, args5?.ToString() ?? NullStr)); } } + + [NonEvent] + internal void TryTraceEvent(string message, T0 args0, T1 args1, T2 args2, T3 args3, T4 args4, T5 args5, T6 arg6) + { + if (Log.IsTraceEnabled()) + { + Trace(string.Format(message, args0?.ToString() ?? NullStr, args1?.ToString() ?? NullStr, args2?.ToString() ?? NullStr, args3?.ToString() ?? NullStr, args4?.ToString() ?? NullStr, args5?.ToString() ?? NullStr, arg6?.ToString() ?? NullStr)); + } + } #endregion #endregion diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommandSet.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommandSet.cs index c1cc23b4ae..fd7dd61e59 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommandSet.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlCommandSet.cs @@ -18,35 +18,16 @@ internal sealed class SqlCommandSet private const string SqlIdentifierPattern = "^@[\\p{Lo}\\p{Lu}\\p{Ll}\\p{Lm}_@#][\\p{Lo}\\p{Lu}\\p{Ll}\\p{Lm}\\p{Nd}\uff3f_@#\\$]*$"; private static readonly Regex s_sqlIdentifierParser = new(SqlIdentifierPattern, RegexOptions.ExplicitCapture | RegexOptions.Singleline | RegexOptions.Compiled); - private List _commandList = new(); - - private SqlCommand _batchCommand; - private static int s_objectTypeCount; // EventSource Counter internal readonly int _objectID = System.Threading.Interlocked.Increment(ref s_objectTypeCount); - private sealed class LocalCommand - { - internal readonly string _commandText; - internal readonly SqlParameterCollection _parameters; - internal readonly int _returnParameterIndex; - internal readonly CommandType _cmdType; - internal readonly SqlCommandColumnEncryptionSetting _columnEncryptionSetting; - - internal LocalCommand(string commandText, SqlParameterCollection parameters, int returnParameterIndex, CommandType cmdType, SqlCommandColumnEncryptionSetting columnEncryptionSetting) - { - Debug.Assert(0 <= commandText.Length, "no text"); - _commandText = commandText; - _parameters = parameters; - _returnParameterIndex = returnParameterIndex; - _cmdType = cmdType; - _columnEncryptionSetting = columnEncryptionSetting; - } - } + private SqlCommand _batchCommand; + private List _commandList; internal SqlCommandSet() : base() { _batchCommand = new SqlCommand(); + _commandList = new List(); } private SqlCommand BatchCommand @@ -64,11 +45,11 @@ private SqlCommand BatchCommand internal int CommandCount => CommandList.Count; - private List CommandList + private List CommandList { get { - List commandList = _commandList; + List commandList = _commandList; if (null == commandList) { throw ADP.ObjectDisposed(this); @@ -204,19 +185,8 @@ internal void Append(SqlCommand command) } } - int returnParameterIndex = -1; - if (null != parameters) - { - for (int i = 0; i < parameters.Count; ++i) - { - if (ParameterDirection.ReturnValue == parameters[i].Direction) - { - returnParameterIndex = i; - break; - } - } - } - LocalCommand cmd = new(cmdText, parameters, returnParameterIndex, command.CommandType, command.ColumnEncryptionSetting); + SqlBatchCommand cmd = new SqlBatchCommand(cmdText, parameters, command.CommandType, command.ColumnEncryptionSetting); + CommandList.Add(cmd); } @@ -255,7 +225,7 @@ internal void Clear() batchCommand.Parameters.Clear(); batchCommand.CommandText = null; } - List commandList = _commandList; + List commandList = _commandList; if (null != commandList) { commandList.Clear(); @@ -291,21 +261,19 @@ internal int ExecuteNonQuery() } ValidateCommandBehavior(nameof(ExecuteNonQuery), CommandBehavior.Default); #endif - BatchCommand.BatchRPCMode = true; - BatchCommand.ClearBatchCommand(); + BatchCommand.SetBatchRPCMode(true); BatchCommand.Parameters.Clear(); - for (int ii = 0; ii < _commandList.Count; ii++) + for (int index = 0; index < _commandList.Count; index++) { - LocalCommand cmd = _commandList[ii]; - BatchCommand.AddBatchCommand(cmd._commandText, cmd._parameters, cmd._cmdType, cmd._columnEncryptionSetting); + BatchCommand.AddBatchCommand(_commandList[index]); } - - return BatchCommand.ExecuteBatchRPCCommand(); + BatchCommand.SetBatchRPCModeReadyToExecute(); + return BatchCommand.ExecuteNonQuery(); } } internal SqlParameter GetParameter(int commandIndex, int parameterIndex) - => CommandList[commandIndex]._parameters[parameterIndex]; + => CommandList[commandIndex].Parameters[parameterIndex]; internal bool GetBatchedAffected(int commandIdentifier, out int recordsAffected, out Exception error) { @@ -316,7 +284,7 @@ internal bool GetBatchedAffected(int commandIdentifier, out int recordsAffected, } internal int GetParameterCount(int commandIndex) - => CommandList[commandIndex]._parameters.Count; + => CommandList[commandIndex].Parameters.Count; private void ValidateCommandBehavior(string method, CommandBehavior behavior) { diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlError.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlError.cs index 2fc7dd605e..6390729bad 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlError.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlError.cs @@ -24,15 +24,33 @@ public sealed class SqlError private readonly int _win32ErrorCode; [System.Runtime.Serialization.OptionalField(VersionAdded = 5)] private readonly Exception _exception; + [System.Runtime.Serialization.OptionalField(VersionAdded = 6)] + private readonly int _batchIndex; + + + // NOTE: do not combine the overloads below using an optional parameter + // they must remain ditinct because external projects use private reflection + // to find and invoke the functions, changing the signatures will break many + // things elsewhere internal SqlError(int infoNumber, byte errorState, byte errorClass, string server, string errorMessage, string procedure, int lineNumber, uint win32ErrorCode, Exception exception = null) - : this(infoNumber, errorState, errorClass, server, errorMessage, procedure, lineNumber, exception) + : this(infoNumber, errorState, errorClass, server, errorMessage, procedure, lineNumber, win32ErrorCode, exception, -1) + { + } + + internal SqlError(int infoNumber, byte errorState, byte errorClass, string server, string errorMessage, string procedure, int lineNumber, uint win32ErrorCode, Exception exception, int batchIndex) + : this(infoNumber, errorState, errorClass, server, errorMessage, procedure, lineNumber, exception, batchIndex) { _server = server; _win32ErrorCode = (int)win32ErrorCode; } internal SqlError(int infoNumber, byte errorState, byte errorClass, string server, string errorMessage, string procedure, int lineNumber, Exception exception = null) + : this(infoNumber, errorState, errorClass, server, errorMessage, procedure, lineNumber, exception, -1) + { + } + + internal SqlError(int infoNumber, byte errorState, byte errorClass, string server, string errorMessage, string procedure, int lineNumber, Exception exception, int batchIndex) { _number = infoNumber; _state = errorState; @@ -43,9 +61,10 @@ internal SqlError(int infoNumber, byte errorState, byte errorClass, string serve _lineNumber = lineNumber; _win32ErrorCode = 0; _exception = exception; + _batchIndex = batchIndex; if (errorClass != 0) { - SqlClientEventSource.Log.TryTraceEvent("SqlError.ctor | ERR | Info Number {0}, Error State {1}, Error Class {2}, Error Message '{3}', Procedure '{4}', Line Number {5}", infoNumber, (int)errorState, (int)errorClass, errorMessage, procedure ?? "None", (int)lineNumber); + SqlClientEventSource.Log.TryTraceEvent("SqlError.ctor | ERR | Info Number {0}, Error State {1}, Error Class {2}, Error Message '{3}', Procedure '{4}', Line Number {5}, Batch Index {6}", infoNumber, (int)errorState, (int)errorClass, errorMessage, procedure ?? "None", (int)lineNumber, batchIndex); } } @@ -87,5 +106,7 @@ public override string ToString() internal int Win32ErrorCode => _win32ErrorCode; internal Exception Exception => _exception; + + internal int BatchIndex => _batchIndex; } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlException.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlException.cs index bbc8670aa8..49215bd2a3 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlException.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlException.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.ComponentModel; +using System.Data.Common; using System.Diagnostics; using System.Globalization; using System.Runtime.Serialization; @@ -25,6 +26,15 @@ public sealed partial class SqlException : System.Data.Common.DbException [System.Runtime.Serialization.OptionalFieldAttribute(VersionAdded = 4)] #endif private Guid _clientConnectionId = Guid.Empty; +#if NETFRAMEWORK + [System.Runtime.Serialization.IgnoreDataMember] +#endif + private SqlBatchCommand _batchCommand; +#if NETFRAMEWORK + [System.Runtime.Serialization.IgnoreDataMember] +#endif + // Do not serialize this field! It is used to indicate that no reconnection attempts are required + internal bool _doNotReconnect = false; private SqlException(string message, SqlErrorCollection errorCollection, Exception innerException, Guid conId) : base(message, innerException) { @@ -99,6 +109,25 @@ public override void GetObjectData(SerializationInfo si, StreamingContext contex /// override public string Source => TdsEnums.SQL_PROVIDER_NAME; + +#if NET6_0_OR_GREATER + /// + protected override DbBatchCommand DbBatchCommand => BatchCommand; + + /// + public new SqlBatchCommand BatchCommand + { + get => _batchCommand; + internal set => _batchCommand = value; + } +#else + internal SqlBatchCommand BatchCommand + { + get => _batchCommand; + set => _batchCommand = value; + } +#endif + /// public override string ToString() { @@ -130,15 +159,25 @@ public override string ToString() return sb.ToString(); } - internal static SqlException CreateException(SqlErrorCollection errorCollection, string serverVersion) - { - return CreateException(errorCollection, serverVersion, Guid.Empty); - } + + // NOTE: do not combine the overloads below using an optional parameter + // they must remain ditinct because external projects use private reflection + // to find and invoke the functions, changing the signatures will break many + // things elsewhere + + internal static SqlException CreateException(SqlErrorCollection errorCollection, string serverVersion) + => CreateException(errorCollection, serverVersion, Guid.Empty, innerException: null, batchCommand: null); + + internal static SqlException CreateException(SqlErrorCollection errorCollection, string serverVersion, SqlBatchCommand batchCommand) + => CreateException(errorCollection, serverVersion, Guid.Empty, innerException: null, batchCommand: batchCommand); internal static SqlException CreateException(SqlErrorCollection errorCollection, string serverVersion, SqlInternalConnectionTds internalConnection, Exception innerException = null) + => CreateException(errorCollection, serverVersion, internalConnection, innerException: innerException, batchCommand: null); + + internal static SqlException CreateException(SqlErrorCollection errorCollection, string serverVersion, SqlInternalConnectionTds internalConnection, Exception innerException = null, SqlBatchCommand batchCommand = null) { Guid connectionId = (internalConnection == null) ? Guid.Empty : internalConnection._clientConnectionId; - SqlException exception = CreateException(errorCollection, serverVersion, connectionId, innerException); + SqlException exception = CreateException(errorCollection, serverVersion, connectionId, innerException, batchCommand); if (internalConnection != null) { @@ -157,6 +196,9 @@ internal static SqlException CreateException(SqlErrorCollection errorCollection, } internal static SqlException CreateException(SqlErrorCollection errorCollection, string serverVersion, Guid conId, Exception innerException = null) + => CreateException(errorCollection, serverVersion, conId, innerException, batchCommand: null); + + internal static SqlException CreateException(SqlErrorCollection errorCollection, string serverVersion, Guid conId, Exception innerException = null, SqlBatchCommand batchCommand = null) { Debug.Assert(null != errorCollection && errorCollection.Count > 0, "no errorCollection?"); @@ -176,9 +218,8 @@ internal static SqlException CreateException(SqlErrorCollection errorCollection, } SqlException exception = new(message.ToString(), errorCollection, innerException, conId); - + exception.BatchCommand = batchCommand; exception.Data.Add("HelpLink.ProdName", "Microsoft SQL Server"); - if (!string.IsNullOrEmpty(serverVersion)) { exception.Data.Add("HelpLink.ProdVer", serverVersion); @@ -201,12 +242,9 @@ internal SqlException InternalClone() exception.Data.Add(entry.Key, entry.Value); } } - + exception._batchCommand = _batchCommand; exception._doNotReconnect = _doNotReconnect; return exception; } - - // Do not serialize this field! It is used to indicate that no reconnection attempts are required - internal bool _doNotReconnect = false; } } diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameterCollection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameterCollection.cs index bc6c19cafb..80f9540a9a 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameterCollection.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlParameterCollection.cs @@ -28,6 +28,11 @@ internal SqlParameterCollection() : base() { } + internal SqlParameterCollection(int capacity) + : this() + { + _items = new List(Math.Max(capacity, 1)); + } /// public new SqlParameter this[int index] diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs index 4c764a2bfa..4355c2ad64 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs @@ -825,7 +825,7 @@ internal void SetTimeoutMilliseconds(long timeout) internal void ThrowExceptionAndWarning(bool callerHasConnectionLock = false, bool asyncClose = false) { - _parser.ThrowExceptionAndWarning(this, callerHasConnectionLock, asyncClose); + _parser.ThrowExceptionAndWarning(this, null, callerHasConnectionLock, asyncClose); } //////////////////////////////////////////// diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlErrorTest.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlErrorTest.cs index c8264eb446..bce79cb6d1 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlErrorTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlErrorTest.cs @@ -61,7 +61,7 @@ private static SqlError CreateError() false, BindingFlags.Instance | BindingFlags.NonPublic, null, - new object[] { 100, (byte)0x00, FATAL_ERROR_CLASS, "ServerName", msg, "ProcedureName", 10, null }, + new object[] { 100, (byte)0x00, FATAL_ERROR_CLASS, "ServerName", msg, "ProcedureName", 10, null, -1 }, null, null); diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj index 8b25a142d4..407dd9af1a 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/Microsoft.Data.SqlClient.ManualTesting.Tests.csproj @@ -249,6 +249,9 @@ + + + diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Batch/BatchTests.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Batch/BatchTests.cs new file mode 100644 index 0000000000..342244ed9c --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/Batch/BatchTests.cs @@ -0,0 +1,623 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Data; +using System.Data.Common; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Data.SqlClient.ManualTesting.Tests +{ + public static class BatchTests + { + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void MissingCommandTextThrows() + { + using (var connection = new SqlConnection(DataTestUtility.TCPConnectionString)) + using (var batch = new SqlBatch { Connection = connection, BatchCommands = { new SqlBatchCommand() } }) + { + connection.Open(); + Assert.Throws(() => batch.ExecuteNonQuery()); + } + } + + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void MissingConnectionThrows() + { + using (var batch = new SqlBatch { BatchCommands = { new SqlBatchCommand("SELECT @@SPID") } }) + { + Assert.Throws(() => batch.ExecuteNonQuery()); + } + } + + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void StoredProcedureBatchSupported() + { + using (var connection = new SqlConnection(DataTestUtility.TCPConnectionString)) + using (var batch = new SqlBatch { Connection = connection, BatchCommands = { new SqlBatchCommand("sp_help", CommandType.StoredProcedure) } }) + { + connection.Open(); + batch.ExecuteNonQuery(); + } + } + + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void CommandTextBatchSupported() + { + using (var connection = new SqlConnection(DataTestUtility.TCPConnectionString)) + using (var batch = new SqlBatch { Connection = connection, BatchCommands = { new SqlBatchCommand("select @@SPID", CommandType.Text) } }) + { + connection.Open(); + batch.ExecuteNonQuery(); + } + } + + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void TableDirectBatchNotSupported() + { + Assert.Throws(() => new SqlBatchCommand("Categories", CommandType.TableDirect)); + } + + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void MixedBatchSupported() + { + using (var connection = new SqlConnection(DataTestUtility.TCPConnectionString)) + using (var batch = new SqlBatch + { + Connection = connection, + BatchCommands = + { + new SqlBatchCommand("select @@SPID", CommandType.Text), + new SqlBatchCommand("sp_help",CommandType.StoredProcedure) + } + }) + { + connection.Open(); + batch.ExecuteNonQuery(); + } + } + + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void DisposedThrows() + { + var batch = new SqlBatch { BatchCommands = { new SqlBatchCommand("SELECT @@SPID") } }; + batch.Dispose(); + Assert.Throws(() => batch.ExecuteNonQuery()); + } + + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void ProviderApi() + { + decimal foundFreight = 0.0m; + int resultCount = 0; + int rowCount = 0; + var dbProviderFactory = SqlClientFactory.Instance; + DbBatch batch = dbProviderFactory.CreateBatch(); + using (var connection = new SqlConnection(DataTestUtility.TCPConnectionString)) + { + connection.Open(); + using (var transaction = connection.BeginTransaction()) + { + { + DbParameter p1 = dbProviderFactory.CreateParameter(); + DbParameter p2 = dbProviderFactory.CreateParameter(); + p1.ParameterName = "@p1"; + p2.ParameterName = "@p2"; + p1.Value = 50.0f; + p2.Value = 10248; + DbBatchCommand command = dbProviderFactory.CreateBatchCommand(); + command.CommandText = "UPDATE Orders SET Freight=@p1 WHERE OrderID=@p2"; + command.Parameters.Add(p1); + command.Parameters.Add(p2); + batch.BatchCommands.Add(command); + } + + { + DbParameter parameter = dbProviderFactory.CreateParameter(); + parameter.ParameterName = "@p4"; + parameter.Value = 10248; + DbBatchCommand command = dbProviderFactory.CreateBatchCommand(); + command.CommandText = $"SELECT Freight FROM Orders WHERE OrderID={parameter.ParameterName}"; + command.Parameters.Add(parameter); + batch.BatchCommands.Add(command); + } + + batch.Connection = connection; + batch.Transaction = transaction; + + try + { + using (var reader = batch.ExecuteReader()) + { + do + { + resultCount += 1; + while (reader.Read()) + { + foundFreight = reader.GetDecimal(0); + rowCount += 1; + } + } + while (reader.NextResult()); + } + } + finally + { + transaction.Rollback(); + } + } + } + + Assert.Equal(1, resultCount); + Assert.Equal(1, rowCount); + Assert.Equal(50.0m, foundFreight); + + Assert.Equal(1, batch.BatchCommands[0].RecordsAffected); + Assert.Equal(0, batch.BatchCommands[1].RecordsAffected); + + } + + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void DirectApi() + { + decimal foundFreight = 0.0m; + int resultCount = 0; + int rowCount = 0; + + SqlException exception = null; + using (var connection = new SqlConnection(DataTestUtility.TCPConnectionString)) + { + connection.Open(); + using (var transaction = connection.BeginTransaction()) + { + var batch = new SqlBatch + { + Connection = connection, + Transaction = transaction, + BatchCommands = + { + new SqlBatchCommand("UPDATE table SET f1=@p1 WHERE f2=@p2") + { + Parameters = + { + new SqlParameter("p1", 8), + new SqlParameter("p2", 9), + } + }, + new SqlBatchCommand("SELECT * FROM [does not exist] WHERE f2=@p1") + { + Parameters = + { + new SqlParameter("p1", 8), + } + } + } + }; + + using (batch) + { + try + { + using (var reader = batch.ExecuteReader()) + { + do + { + resultCount += 1; + while (reader.Read()) + { + foundFreight = reader.GetDecimal(0); + rowCount += 1; + } + } + while (reader.NextResult()); + } + } + catch (SqlException sqlex) + { + exception = sqlex; + } + finally + { + transaction.Rollback(); + } + } + } + } + + Assert.NotNull(exception); + Assert.NotNull(exception.BatchCommand); + } + + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void ExceptionInBatchContainsBatch() + { + SqlException exception = null; + using (var connection = new SqlConnection(DataTestUtility.TCPConnectionString)) + { + connection.Open(); + using (var transaction = connection.BeginTransaction()) + { + using (var batch = new SqlBatch + { + Connection = connection, + Transaction = transaction, + BatchCommands = { new SqlBatchCommand("RAISERROR ( 'an intentional error occured.', 15, 1)") } + } + ) + { + try + { + batch.ExecuteNonQuery(); + } + catch (SqlException sqlex) + { + exception = sqlex; + } + finally + { + transaction.Rollback(); + } + } + } + } + + Assert.NotNull(exception); + Assert.NotNull(exception.BatchCommand); + } + + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void ExceptionWithoutBatchContainsNoBatch() + { + SqlException exception = null; + using (var connection = new SqlConnection(DataTestUtility.TCPConnectionString)) + { + connection.Open(); + using (var transaction = connection.BeginTransaction()) + using (var command = new SqlCommand("RAISERROR ( 'an intentional error occured.', 15, 1)", connection, transaction)) + { + try + { + command.ExecuteNonQuery(); + } + catch (SqlException sqlex) + { + exception = sqlex; + } + finally + { + transaction.Rollback(); + } + } + } + + Assert.NotNull(exception); + Assert.Null(exception.BatchCommand); + } + + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void ParameterInOutAndReturn() + { + string create = + @" +CREATE PROCEDURE TestInAndOutParams + @Input int, + @InOut int OUTPUT, + @Output int = default OUTPUT +AS +BEGIN + SET NOCOUNT ON; + SELECT @InOut = 2 * @InOut, @Output = 2 * @Input + RETURN @Input +END"; + string drop = "DROP PROCEDURE TestInAndOutParams"; + + SqlParameter input = CreateParameter("@Input", SqlDbType.Int, 2); + SqlParameter inputOutput = CreateParameter("@InOut", SqlDbType.Int, 4, ParameterDirection.InputOutput); + SqlParameter output = CreateParameter("@Output", SqlDbType.Int, DBNull.Value, ParameterDirection.Output); + SqlParameter returned = CreateParameter("@RETURN_VALUE", SqlDbType.Int, DBNull.Value, ParameterDirection.ReturnValue); + try + { + TryExecuteNonQueryCommand(drop); + ExecuteNonQueryCommand(create); + + using (SqlConnection conn = new SqlConnection(DataTestUtility.TCPConnectionString)) + using (SqlBatch batch = new SqlBatch(conn)) + { + conn.Open(); + batch.Commands.Add(new SqlBatchCommand("SELECT @@VERSION")); + batch.Commands.Add( + new SqlBatchCommand( + "TestInAndOutParams", + CommandType.StoredProcedure, + new[] { input, inputOutput, output, returned } + ) + ); + batch.Commands.Add(new SqlBatchCommand("SELECT @@SPID")); + batch.ExecuteNonQuery(); + } + } + finally + { + TryExecuteNonQueryCommand(drop); + } + + Assert.Equal(8, Convert.ToInt32(inputOutput.Value)); + Assert.Equal(4, Convert.ToInt32(output.Value)); + Assert.Equal(2, Convert.ToInt32(returned.Value)); + } + + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void ExecuteNonQuery() + { + int count = 0; + + var batch = new SqlBatch + { + BatchCommands = + { + new SqlBatchCommand("UPDATE Orders SET Freight=@p1 WHERE OrderID=@p2") + { + Parameters = + { + new SqlParameter("@p1", 50.0f), + new SqlParameter("@p2", 10248), + } + }, + new SqlBatchCommand($"UPDATE Orders SET Freight=@p1 WHERE OrderID=@p2") + { + Parameters = + { + new SqlParameter("@p1", 36.0f), + new SqlParameter("@p2", -10248), + } + }, + new SqlBatchCommand($"UPDATE Orders SET Freight=@p1 WHERE OrderID=@p2") + { + Parameters = + { + new SqlParameter("@p1", 90.0f), + new SqlParameter("@p2", 10248), + } + } + } + }; + using (var connection = new SqlConnection(DataTestUtility.TCPConnectionString)) + { + connection.Open(); + using (var transaction = connection.BeginTransaction()) + { + + batch.Connection = connection; + batch.Transaction = transaction; + + try + { + count = batch.ExecuteNonQuery(); + } + finally + { + transaction.Rollback(); + } + } + } + + Assert.Equal(3, batch.Commands.Count); + Assert.Equal(2, count); + Assert.Equal(1, batch.Commands[0].RecordsAffected); + Assert.Equal(0, batch.Commands[1].RecordsAffected); + Assert.Equal(1, batch.Commands[2].RecordsAffected); + } + + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static async Task ExecuteNonQueryAsync() + { + int count = 0; + + var batch = new SqlBatch + { + BatchCommands = + { + new SqlBatchCommand("UPDATE Orders SET Freight=@p1 WHERE OrderID=@p2") + { + Parameters = + { + new SqlParameter("@p1", 50.0f), + new SqlParameter("@p2", 10248), + } + }, + new SqlBatchCommand($"UPDATE Orders SET Freight=@p1 WHERE OrderID=@p2") + { + Parameters = + { + new SqlParameter("@p1", 36.0f), + new SqlParameter("@p2", -10248), + } + }, + new SqlBatchCommand($"UPDATE Orders SET Freight=@p1 WHERE OrderID=@p2") + { + Parameters = + { + new SqlParameter("@p1", 90.0f), + new SqlParameter("@p2", 10248), + } + } + } + }; + using (var connection = new SqlConnection(DataTestUtility.TCPConnectionString)) + { + await connection.OpenAsync(); + using (var transaction = connection.BeginTransaction()) + { + + batch.Connection = connection; + batch.Transaction = (SqlTransaction)transaction; + + try + { + count = await batch.ExecuteNonQueryAsync(); + } + finally + { + transaction.Rollback(); + } + } + } + + Assert.Equal(3, batch.Commands.Count); + Assert.Equal(2, count); + Assert.Equal(1, batch.Commands[0].RecordsAffected); + Assert.Equal(0, batch.Commands[1].RecordsAffected); + Assert.Equal(1, batch.Commands[2].RecordsAffected); + } + + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void ExecuteScalarMultiple() + { + int value = 0; + + using (SqlConnection conn = new SqlConnection(DataTestUtility.TCPConnectionString)) + using (SqlBatch batch = new SqlBatch(conn)) + { + conn.Open(); + for (int index = 0; index < 10; index++) + { + batch.Commands.Add(new SqlBatchCommand($"SELECT {index}")); + } + value = Convert.ToInt32(batch.ExecuteScalar()); + } + + Assert.Equal(9, value); + } + + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static async Task ExecuteScalarAsyncMultiple() + { + int value = 0; + + using (SqlConnection conn = new SqlConnection(DataTestUtility.TCPConnectionString)) + using (SqlBatch batch = new SqlBatch(conn)) + { + await conn.OpenAsync(); + for (int index = 0; index < 10; index++) + { + batch.Commands.Add(new SqlBatchCommand($"SELECT {index}")); + } + value = Convert.ToInt32(await batch.ExecuteScalarAsync()); + } + + Assert.Equal(9, value); + } + + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void ExecuteReaderMultiple() + { + int resultSetCount = 0; + int resultRowCount = 0; + + using (SqlConnection conn = new SqlConnection(DataTestUtility.TCPConnectionString)) + using (SqlBatch batch = new SqlBatch(conn)) + { + conn.Open(); + for (int index = 0; index < 10; index++) + { + batch.Commands.Add(new SqlBatchCommand($"SELECT {index}")); + } + using (var reader = batch.ExecuteReader()) + { + do + { + resultSetCount += 1; + while (reader.Read()) + { + resultRowCount += 1; + } + } while (reader.NextResult()); + } + } + + Assert.Equal(10, resultSetCount); + Assert.Equal(10, resultRowCount); + } + + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static async Task ExecuteReaderAsyncMultiple() + { + int resultSetCount = 0; + int resultRowCount = 0; + + using (SqlConnection conn = new SqlConnection(DataTestUtility.TCPConnectionString)) + using (SqlBatch batch = new SqlBatch(conn)) + { + await conn.OpenAsync(); + for (int index = 0; index < 10; index++) + { + batch.Commands.Add(new SqlBatchCommand($"SELECT {index}")); + } + using (var reader = await batch.ExecuteReaderAsync()) + { + do + { + resultSetCount += 1; + while (await reader.ReadAsync()) + { + resultRowCount += 1; + } + } while (await reader.NextResultAsync()); + } + } + + Assert.Equal(10, resultSetCount); + Assert.Equal(10, resultRowCount); + } + + private static SqlParameter CreateParameter(string name, SqlDbType type, T value, ParameterDirection direction = ParameterDirection.Input) + { + var parameter = new SqlParameter(name, type); + parameter.Direction = direction; + parameter.Value = value; + return parameter; + } + + private static void ExecuteNonQueryCommand(string command) + { + using (SqlConnection conn = new SqlConnection(DataTestUtility.TCPConnectionString)) + using (SqlCommand cmd = conn.CreateCommand()) + { + conn.Open(); + cmd.CommandText = command; + cmd.ExecuteNonQuery(); + } + } + private static bool TryExecuteNonQueryCommand(string command) + { + try + { + ExecuteNonQueryCommand(command); + return true; + } + catch + { + } + return false; + } + } +}