From 1be606c7ebc46b73ef6cce41f75ede7e0fd278ac Mon Sep 17 00:00:00 2001 From: Johnny Pham <23270162+johnnypham@users.noreply.github.com> Date: Tue, 9 Jun 2020 18:48:20 -0700 Subject: [PATCH 1/5] Optional ORDER hints for SqlBulkCopy (#540) --- doc/samples/SqlBulkCopy_ColumnOrderHint.cs | 84 +++ .../SqlBulkCopy_ColumnOrderHintCollection.cs | 83 +++ ...qlBulkCopy_ColumnOrderHintCollectionAdd.cs | 86 +++ ...lBulkCopy_ColumnOrderHintCollectionAdd2.cs | 83 +++ ...BulkCopy_ColumnOrderHintCollectionClear.cs | 177 ++++++ ...ulkCopy_ColumnOrderHintCollectionRemove.cs | 176 ++++++ ...kCopy_ColumnOrderHintCollectionRemoveAt.cs | 176 ++++++ .../SqlBulkCopy_ColumnOrderHintColumn.cs | 85 +++ .../SqlBulkCopy_ColumnOrderHintSortOrder.cs | 85 +++ .../Microsoft.Data.SqlClient/SqlBulkCopy.xml | 79 ++- .../SqlBulkCopyColumnOrderHint.xml | 131 ++++ .../SqlBulkCopyColumnOrderHintCollection.xml | 226 +++++++ .../netcore/ref/Microsoft.Data.SqlClient.cs | 36 ++ .../src/Microsoft.Data.SqlClient.csproj | 6 + .../Microsoft/Data/SqlClient/SqlBulkCopy.cs | 47 +- .../src/Microsoft/Data/SqlClient/SqlUtil.cs | 20 + .../netcore/src/Resources/SR.Designer.cs | 36 ++ .../netcore/src/Resources/SR.resx | 14 +- .../netfx/ref/Microsoft.Data.SqlClient.cs | 36 ++ .../netfx/src/Microsoft.Data.SqlClient.csproj | 6 + .../Microsoft/Data/SqlClient/SqlBulkCopy.cs | 65 +- .../src/Microsoft/Data/SqlClient/SqlUtil.cs | 20 + .../netfx/src/Resources/Strings.Designer.cs | 36 ++ .../netfx/src/Resources/Strings.resx | 14 +- .../SqlClient/SqlBulkCopyColumnOrderHint.cs | 66 ++ .../SqlBulkCopyColumnOrderHintCollection.cs | 133 ++++ .../Microsoft.Data.SqlClient.Tests.csproj | 1 + ...qlBulkCopyColumnOrderHintCollectionTest.cs | 583 ++++++++++++++++++ ....Data.SqlClient.ManualTesting.Tests.csproj | 6 + .../SQL/SqlBulkCopyTest/OrderHint.cs | 150 +++++ .../SQL/SqlBulkCopyTest/OrderHintAsync.cs | 132 ++++ .../OrderHintDuplicateColumn.cs | 70 +++ .../OrderHintIdentityColumn.cs | 67 ++ .../OrderHintMissingTargetColumn.cs | 75 +++ .../SqlBulkCopyTest/OrderHintTransaction.cs | 58 ++ .../SQL/SqlBulkCopyTest/SqlBulkCopyTest.cs | 37 ++ 36 files changed, 3167 insertions(+), 18 deletions(-) create mode 100644 doc/samples/SqlBulkCopy_ColumnOrderHint.cs create mode 100644 doc/samples/SqlBulkCopy_ColumnOrderHintCollection.cs create mode 100644 doc/samples/SqlBulkCopy_ColumnOrderHintCollectionAdd.cs create mode 100644 doc/samples/SqlBulkCopy_ColumnOrderHintCollectionAdd2.cs create mode 100644 doc/samples/SqlBulkCopy_ColumnOrderHintCollectionClear.cs create mode 100644 doc/samples/SqlBulkCopy_ColumnOrderHintCollectionRemove.cs create mode 100644 doc/samples/SqlBulkCopy_ColumnOrderHintCollectionRemoveAt.cs create mode 100644 doc/samples/SqlBulkCopy_ColumnOrderHintColumn.cs create mode 100644 doc/samples/SqlBulkCopy_ColumnOrderHintSortOrder.cs create mode 100644 doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopyColumnOrderHint.xml create mode 100644 doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopyColumnOrderHintCollection.xml create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopyColumnOrderHint.cs create mode 100644 src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopyColumnOrderHintCollection.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlBulkCopyColumnOrderHintCollectionTest.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHint.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintAsync.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintDuplicateColumn.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintIdentityColumn.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintMissingTargetColumn.cs create mode 100644 src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintTransaction.cs diff --git a/doc/samples/SqlBulkCopy_ColumnOrderHint.cs b/doc/samples/SqlBulkCopy_ColumnOrderHint.cs new file mode 100644 index 0000000000..9960debf20 --- /dev/null +++ b/doc/samples/SqlBulkCopy_ColumnOrderHint.cs @@ -0,0 +1,84 @@ +// +using System; +using System.Data; +using Microsoft.Data.SqlClient; + +class Program +{ + static void Main() + { + string connectionString = GetConnectionString(); + // Open a sourceConnection to the AdventureWorks database. + using (SqlConnection sourceConnection = + new SqlConnection(connectionString)) + { + sourceConnection.Open(); + + // Perform an initial count on the destination table. + SqlCommand commandRowCount = new SqlCommand( + "SELECT COUNT(*) FROM " + + "dbo.BulkCopyDemoMatchingColumns;", + sourceConnection); + long countStart = System.Convert.ToInt32( + commandRowCount.ExecuteScalar()); + Console.WriteLine("Starting row count = {0}", countStart); + + // Get data from the source table as a SqlDataReader. + SqlCommand commandSourceData = new SqlCommand( + "SELECT ProductID, Name, " + + "ProductNumber " + + "FROM Production.Product;", sourceConnection); + SqlDataReader reader = + commandSourceData.ExecuteReader(); + + // Set up the bulk copy object. + using (SqlBulkCopy bulkCopy = + new SqlBulkCopy(connectionString)) + { + bulkCopy.DestinationTableName = + "dbo.BulkCopyDemoMatchingColumns"; + + // Setup an order hint for the ProductNumber column. + SqlBulkCopyColumnOrderHint hintNumber = + new SqlBulkCopyColumnOrderHint("ProductNumber", SortOrder.Ascending); + bulkCopy.ColumnOrderHints.Add(hintNumber); + + // Write from the source to the destination. + try + { + bulkCopy.WriteToServer(reader); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + finally + { + // Close the SqlDataReader. The SqlBulkCopy + // object is automatically closed at the end + // of the using block. + reader.Close(); + } + } + + // Perform a final count on the destination + // table to see how many rows were added. + long countEnd = System.Convert.ToInt32( + commandRowCount.ExecuteScalar()); + Console.WriteLine("Ending row count = {0}", countEnd); + Console.WriteLine("{0} rows were added.", countEnd - countStart); + Console.WriteLine("Press Enter to finish."); + Console.ReadLine(); + } + } + + private static string GetConnectionString() + // To avoid storing the sourceConnection string in your code, + // you can retrieve it from a configuration file. + { + return "Data Source=(local); " + + " Integrated Security=true;" + + "Initial Catalog=AdventureWorks;"; + } +} +// diff --git a/doc/samples/SqlBulkCopy_ColumnOrderHintCollection.cs b/doc/samples/SqlBulkCopy_ColumnOrderHintCollection.cs new file mode 100644 index 0000000000..4a92099ddb --- /dev/null +++ b/doc/samples/SqlBulkCopy_ColumnOrderHintCollection.cs @@ -0,0 +1,83 @@ +// +using System; +using System.Data; +using Microsoft.Data.SqlClient; + +class Program +{ + static void Main() + { + string connectionString = GetConnectionString(); + // Open a sourceConnection to the AdventureWorks database. + using (SqlConnection sourceConnection = + new SqlConnection(connectionString)) + { + sourceConnection.Open(); + + // Perform an initial count on the destination table. + SqlCommand commandRowCount = new SqlCommand( + "SELECT COUNT(*) FROM " + + "dbo.BulkCopyDemoMatchingColumns;", + sourceConnection); + long countStart = System.Convert.ToInt32( + commandRowCount.ExecuteScalar()); + Console.WriteLine("Starting row count = {0}", countStart); + + // Get data from the source table as a SqlDataReader. + SqlCommand commandSourceData = new SqlCommand( + "SELECT ProductID, Name, " + + "ProductNumber " + + "FROM Production.Product;", sourceConnection); + SqlDataReader reader = + commandSourceData.ExecuteReader(); + + // Set up the bulk copy object. + using (SqlBulkCopy bulkCopy = + new SqlBulkCopy(connectionString)) + { + bulkCopy.DestinationTableName = + "dbo.BulkCopyDemoMatchingColumns"; + + // Specify the sort order for the ProductNumber column in + // the destination table. + bulkCopy.ColumnOrderHints.Add("ProductNumber", SortOrder.Ascending); + + // Write from the source to the destination. + try + { + bulkCopy.WriteToServer(reader); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + finally + { + // Close the SqlDataReader. The SqlBulkCopy + // object is automatically closed at the end + // of the using block. + reader.Close(); + } + } + + // Perform a final count on the destination + // table to see how many rows were added. + long countEnd = System.Convert.ToInt32( + commandRowCount.ExecuteScalar()); + Console.WriteLine("Ending row count = {0}", countEnd); + Console.WriteLine("{0} rows were added.", countEnd - countStart); + Console.WriteLine("Press Enter to finish."); + Console.ReadLine(); + } + } + + private static string GetConnectionString() + // To avoid storing the sourceConnection string in your code, + // you can retrieve it from a configuration file. + { + return "Data Source=(local); " + + " Integrated Security=true;" + + "Initial Catalog=AdventureWorks;"; + } +} +// diff --git a/doc/samples/SqlBulkCopy_ColumnOrderHintCollectionAdd.cs b/doc/samples/SqlBulkCopy_ColumnOrderHintCollectionAdd.cs new file mode 100644 index 0000000000..2d6599d525 --- /dev/null +++ b/doc/samples/SqlBulkCopy_ColumnOrderHintCollectionAdd.cs @@ -0,0 +1,86 @@ +// +using System; +using System.Data; +using Microsoft.Data.SqlClient; + +class Program +{ + static void Main() + { + string connectionString = GetConnectionString(); + // Open a sourceConnection to the AdventureWorks database. + using (SqlConnection sourceConnection = + new SqlConnection(connectionString)) + { + sourceConnection.Open(); + + // Perform an initial count on the destination table. + SqlCommand commandRowCount = new SqlCommand( + "SELECT COUNT(*) FROM " + + "dbo.BulkCopyDemoMatchingColumns;", + sourceConnection); + long countStart = System.Convert.ToInt32( + commandRowCount.ExecuteScalar()); + Console.WriteLine("Starting row count = {0}", countStart); + + // Get data from the source table as a SqlDataReader. + SqlCommand commandSourceData = new SqlCommand( + "SELECT ProductID, Name, " + + "ProductNumber " + + "FROM Production.Product;", sourceConnection); + SqlDataReader reader = + commandSourceData.ExecuteReader(); + + // Set up the bulk copy object. + using (SqlBulkCopy bulkCopy = + new SqlBulkCopy(connectionString)) + { + bulkCopy.DestinationTableName = + "dbo.BulkCopyDemoMatchingColumns"; + + // Specify the sort order for the ProductNumber column in + // the destination table. + // Setup an order hint for the ProductNumber column. + SqlBulkCopyColumnOrderHint hintNumber = + new SqlBulkCopyColumnOrderHint("ProductNumber", SortOrder.Ascending); + bulkCopy.ColumnOrderHints.Add(hintNumber); + + // Write from the source to the destination. + try + { + bulkCopy.WriteToServer(reader); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + finally + { + // Close the SqlDataReader. The SqlBulkCopy + // object is automatically closed at the end + // of the using block. + reader.Close(); + } + } + + // Perform a final count on the destination + // table to see how many rows were added. + long countEnd = System.Convert.ToInt32( + commandRowCount.ExecuteScalar()); + Console.WriteLine("Ending row count = {0}", countEnd); + Console.WriteLine("{0} rows were added.", countEnd - countStart); + Console.WriteLine("Press Enter to finish."); + Console.ReadLine(); + } + } + + private static string GetConnectionString() + // To avoid storing the sourceConnection string in your code, + // you can retrieve it from a configuration file. + { + return "Data Source=(local); " + + " Integrated Security=true;" + + "Initial Catalog=AdventureWorks;"; + } +} +// diff --git a/doc/samples/SqlBulkCopy_ColumnOrderHintCollectionAdd2.cs b/doc/samples/SqlBulkCopy_ColumnOrderHintCollectionAdd2.cs new file mode 100644 index 0000000000..4a92099ddb --- /dev/null +++ b/doc/samples/SqlBulkCopy_ColumnOrderHintCollectionAdd2.cs @@ -0,0 +1,83 @@ +// +using System; +using System.Data; +using Microsoft.Data.SqlClient; + +class Program +{ + static void Main() + { + string connectionString = GetConnectionString(); + // Open a sourceConnection to the AdventureWorks database. + using (SqlConnection sourceConnection = + new SqlConnection(connectionString)) + { + sourceConnection.Open(); + + // Perform an initial count on the destination table. + SqlCommand commandRowCount = new SqlCommand( + "SELECT COUNT(*) FROM " + + "dbo.BulkCopyDemoMatchingColumns;", + sourceConnection); + long countStart = System.Convert.ToInt32( + commandRowCount.ExecuteScalar()); + Console.WriteLine("Starting row count = {0}", countStart); + + // Get data from the source table as a SqlDataReader. + SqlCommand commandSourceData = new SqlCommand( + "SELECT ProductID, Name, " + + "ProductNumber " + + "FROM Production.Product;", sourceConnection); + SqlDataReader reader = + commandSourceData.ExecuteReader(); + + // Set up the bulk copy object. + using (SqlBulkCopy bulkCopy = + new SqlBulkCopy(connectionString)) + { + bulkCopy.DestinationTableName = + "dbo.BulkCopyDemoMatchingColumns"; + + // Specify the sort order for the ProductNumber column in + // the destination table. + bulkCopy.ColumnOrderHints.Add("ProductNumber", SortOrder.Ascending); + + // Write from the source to the destination. + try + { + bulkCopy.WriteToServer(reader); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + finally + { + // Close the SqlDataReader. The SqlBulkCopy + // object is automatically closed at the end + // of the using block. + reader.Close(); + } + } + + // Perform a final count on the destination + // table to see how many rows were added. + long countEnd = System.Convert.ToInt32( + commandRowCount.ExecuteScalar()); + Console.WriteLine("Ending row count = {0}", countEnd); + Console.WriteLine("{0} rows were added.", countEnd - countStart); + Console.WriteLine("Press Enter to finish."); + Console.ReadLine(); + } + } + + private static string GetConnectionString() + // To avoid storing the sourceConnection string in your code, + // you can retrieve it from a configuration file. + { + return "Data Source=(local); " + + " Integrated Security=true;" + + "Initial Catalog=AdventureWorks;"; + } +} +// diff --git a/doc/samples/SqlBulkCopy_ColumnOrderHintCollectionClear.cs b/doc/samples/SqlBulkCopy_ColumnOrderHintCollectionClear.cs new file mode 100644 index 0000000000..0df1281b52 --- /dev/null +++ b/doc/samples/SqlBulkCopy_ColumnOrderHintCollectionClear.cs @@ -0,0 +1,177 @@ +// +using System; +using System.Data; +using Microsoft.Data.SqlClient; + +class Program +{ + static void Main() + { + string connectionString = GetConnectionString(); + // Open a connection to the AdventureWorks database. + using (SqlConnection connection = + new SqlConnection(connectionString)) + { + connection.Open(); + + // Empty the destination tables. + SqlCommand deleteHeader = new SqlCommand( + "DELETE FROM dbo.BulkCopyDemoOrderHeader;", + connection); + deleteHeader.ExecuteNonQuery(); + SqlCommand deleteDetail = new SqlCommand( + "DELETE FROM dbo.BulkCopyDemoOrderDetail;", + connection); + deleteDetail.ExecuteNonQuery(); + + // Perform an initial count on the destination + // table with matching columns. + SqlCommand countRowHeader = new SqlCommand( + "SELECT COUNT(*) FROM dbo.BulkCopyDemoOrderHeader;", + connection); + long countStartHeader = System.Convert.ToInt32( + countRowHeader.ExecuteScalar()); + Console.WriteLine( + "Starting row count for Header table = {0}", + countStartHeader); + + // Perform an initial count on the destination + // table with different column positions. + SqlCommand countRowDetail = new SqlCommand( + "SELECT COUNT(*) FROM dbo.BulkCopyDemoOrderDetail;", + connection); + long countStartDetail = System.Convert.ToInt32( + countRowDetail.ExecuteScalar()); + Console.WriteLine( + "Starting row count for Detail table = {0}", + countStartDetail); + + // Get data from the source table as a SqlDataReader. + // The Sales.SalesOrderHeader and Sales.SalesOrderDetail + // tables are quite large and could easily cause a timeout + // if all data from the tables is added to the destination. + // To keep the example simple and quick, a parameter is + // used to select only orders for a particular account + // as the source for the bulk insert. + SqlCommand headerData = new SqlCommand( + "SELECT [SalesOrderID], [OrderDate], " + + "[AccountNumber] FROM [Sales].[SalesOrderHeader] " + + "WHERE [AccountNumber] = @accountNumber;", + connection); + SqlParameter parameterAccount = new SqlParameter(); + parameterAccount.ParameterName = "@accountNumber"; + parameterAccount.SqlDbType = SqlDbType.NVarChar; + parameterAccount.Direction = ParameterDirection.Input; + parameterAccount.Value = "10-4020-000034"; + headerData.Parameters.Add(parameterAccount); + SqlDataReader readerHeader = headerData.ExecuteReader(); + + // Get the Detail data in a separate connection. + using (SqlConnection connection2 = new SqlConnection(connectionString)) + { + connection2.Open(); + SqlCommand sourceDetailData = new SqlCommand( + "SELECT [Sales].[SalesOrderDetail].[SalesOrderID], [SalesOrderDetailID], " + + "[OrderQty], [ProductID], [UnitPrice] FROM [Sales].[SalesOrderDetail] " + + "INNER JOIN [Sales].[SalesOrderHeader] ON [Sales].[SalesOrderDetail]." + + "[SalesOrderID] = [Sales].[SalesOrderHeader].[SalesOrderID] " + + "WHERE [AccountNumber] = @accountNumber;", connection2); + + SqlParameter accountDetail = new SqlParameter(); + accountDetail.ParameterName = "@accountNumber"; + accountDetail.SqlDbType = SqlDbType.NVarChar; + accountDetail.Direction = ParameterDirection.Input; + accountDetail.Value = "10-4020-000034"; + sourceDetailData.Parameters.Add(accountDetail); + SqlDataReader readerDetail = sourceDetailData.ExecuteReader(); + + // Create the SqlBulkCopy object. + using (SqlBulkCopy bulkCopy = + new SqlBulkCopy(connectionString)) + { + bulkCopy.DestinationTableName = + "dbo.BulkCopyDemoOrderHeader"; + + // Guarantee that columns are mapped correctly by + // defining the column mappings for the order. + bulkCopy.ColumnMappings.Add("SalesOrderID", "SalesOrderID"); + bulkCopy.ColumnMappings.Add("OrderDate", "OrderDate"); + bulkCopy.ColumnMappings.Add("AccountNumber", "AccountNumber"); + + // Add order hint for OrderDate column. + bulkCopy.ColumnOrderHints.Add("OrderDate", SortOrder.Ascending); + + // Write readerHeader to the destination. + try + { + bulkCopy.WriteToServer(readerHeader); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + finally + { + readerHeader.Close(); + } + + // Set up the order details destination. + bulkCopy.DestinationTableName = "dbo.BulkCopyDemoOrderDetail"; + + // Clear the ColumnMappingCollection. + bulkCopy.ColumnMappings.Clear(); + + // Add order detail column mappings. + bulkCopy.ColumnMappings.Add("SalesOrderID", "SalesOrderID"); + bulkCopy.ColumnMappings.Add("SalesOrderDetailID", "SalesOrderDetailID"); + bulkCopy.ColumnMappings.Add("OrderQty", "OrderQty"); + bulkCopy.ColumnMappings.Add("ProductID", "ProductID"); + bulkCopy.ColumnMappings.Add("UnitPrice", "UnitPrice"); + + // Clear the ColumnOrderHintCollection. + bulkCopy.ColumnOrderHints.Clear(); + + // Add order hint for SalesOrderID column. + bulkCopy.ColumnOrderHints.Add("SalesOrderID", SortOrder.Ascending); + + // Write readerDetail to the destination. + try + { + bulkCopy.WriteToServer(readerDetail); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + finally + { + readerDetail.Close(); + } + } + + // Perform a final count on the destination + // tables to see how many rows were added. + long countEndHeader = System.Convert.ToInt32( + countRowHeader.ExecuteScalar()); + Console.WriteLine("{0} rows were added to the Header table.", + countEndHeader - countStartHeader); + long countEndDetail = System.Convert.ToInt32( + countRowDetail.ExecuteScalar()); + Console.WriteLine("{0} rows were added to the Detail table.", + countEndDetail - countStartDetail); + Console.WriteLine("Press Enter to finish."); + Console.ReadLine(); + } + } + } + + private static string GetConnectionString() + // To avoid storing the connection string in your code, + // you can retrieve it from a configuration file. + { + return "Data Source=(local); " + + " Integrated Security=true;" + + "Initial Catalog=AdventureWorks;"; + } +} +// diff --git a/doc/samples/SqlBulkCopy_ColumnOrderHintCollectionRemove.cs b/doc/samples/SqlBulkCopy_ColumnOrderHintCollectionRemove.cs new file mode 100644 index 0000000000..ea9a8f4a46 --- /dev/null +++ b/doc/samples/SqlBulkCopy_ColumnOrderHintCollectionRemove.cs @@ -0,0 +1,176 @@ +// +using System; +using System.Data; +using Microsoft.Data.SqlClient; + +class Program +{ + static void Main() + { + string connectionString = GetConnectionString(); + // Open a connection to the AdventureWorks database. + using (SqlConnection connection = + new SqlConnection(connectionString)) + { + connection.Open(); + + // Empty the destination tables. + SqlCommand deleteHeader = new SqlCommand( + "DELETE FROM dbo.BulkCopyDemoOrderHeader;", + connection); + deleteHeader.ExecuteNonQuery(); + SqlCommand deleteDetail = new SqlCommand( + "DELETE FROM dbo.BulkCopyDemoOrderDetail;", + connection); + deleteDetail.ExecuteNonQuery(); + + // Perform an initial count on the destination + // table with matching columns. + SqlCommand countRowHeader = new SqlCommand( + "SELECT COUNT(*) FROM dbo.BulkCopyDemoOrderHeader;", + connection); + long countStartHeader = System.Convert.ToInt32( + countRowHeader.ExecuteScalar()); + Console.WriteLine( + "Starting row count for Header table = {0}", + countStartHeader); + + // Perform an initial count on the destination + // table with different column positions. + SqlCommand countRowDetail = new SqlCommand( + "SELECT COUNT(*) FROM dbo.BulkCopyDemoOrderDetail;", + connection); + long countStartDetail = System.Convert.ToInt32( + countRowDetail.ExecuteScalar()); + Console.WriteLine( + "Starting row count for Detail table = {0}", + countStartDetail); + + // Get data from the source table as a SqlDataReader. + // The Sales.SalesOrderHeader and Sales.SalesOrderDetail + // tables are quite large and could easily cause a timeout + // if all data from the tables is added to the destination. + // To keep the example simple and quick, a parameter is + // used to select only orders for a particular account + // as the source for the bulk insert. + SqlCommand headerData = new SqlCommand( + "SELECT [SalesOrderID], [OrderDate], " + + "[AccountNumber] FROM [Sales].[SalesOrderHeader] " + + "WHERE [AccountNumber] = @accountNumber;", + connection); + SqlParameter parameterAccount = new SqlParameter(); + parameterAccount.ParameterName = "@accountNumber"; + parameterAccount.SqlDbType = SqlDbType.NVarChar; + parameterAccount.Direction = ParameterDirection.Input; + parameterAccount.Value = "10-4020-000034"; + headerData.Parameters.Add(parameterAccount); + SqlDataReader readerHeader = headerData.ExecuteReader(); + + // Get the Detail data in a separate connection. + using (SqlConnection connection2 = new SqlConnection(connectionString)) + { + connection2.Open(); + SqlCommand sourceDetailData = new SqlCommand( + "SELECT [Sales].[SalesOrderDetail].[SalesOrderID], [SalesOrderDetailID], " + + "[OrderQty], [ProductID], [UnitPrice] FROM [Sales].[SalesOrderDetail] " + + "INNER JOIN [Sales].[SalesOrderHeader] ON [Sales].[SalesOrderDetail]." + + "[SalesOrderID] = [Sales].[SalesOrderHeader].[SalesOrderID] " + + "WHERE [AccountNumber] = @accountNumber;", connection2); + + SqlParameter accountDetail = new SqlParameter(); + accountDetail.ParameterName = "@accountNumber"; + accountDetail.SqlDbType = SqlDbType.NVarChar; + accountDetail.Direction = ParameterDirection.Input; + accountDetail.Value = "10-4020-000034"; + sourceDetailData.Parameters.Add(accountDetail); + SqlDataReader readerDetail = sourceDetailData.ExecuteReader(); + + // Create the SqlBulkCopy object. + using (SqlBulkCopy bulkCopy = + new SqlBulkCopy(connectionString)) + { + bulkCopy.DestinationTableName = + "dbo.BulkCopyDemoOrderHeader"; + + // Guarantee that columns are mapped correctly by + // defining the column mappings for the order. + bulkCopy.ColumnMappings.Add("SalesOrderID", "SalesOrderID"); + bulkCopy.ColumnMappings.Add("OrderDate", "OrderDate"); + bulkCopy.ColumnMappings.Add("AccountNumber", "AccountNumber"); + + // Add the order hint for the OrderDate column. + SqlBulkCopyColumnOrderHint hintDate = + new SqlBulkCopyColumnOrderHint("OrderDate", SortOrder.Ascending); + bulkCopy.ColumnOrderHints.Add(hintDate); + + // Write readerHeader to the destination. + try + { + bulkCopy.WriteToServer(readerHeader); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + finally + { + readerHeader.Close(); + } + + // Set up the order details destination. + bulkCopy.DestinationTableName = "dbo.BulkCopyDemoOrderDetail"; + + // Clear the ColumnMappingCollection. + bulkCopy.ColumnMappings.Clear(); + + // Add order detail column mappings. + bulkCopy.ColumnMappings.Add("SalesOrderID", "SalesOrderID"); + bulkCopy.ColumnMappings.Add("SalesOrderDetailID", "SalesOrderDetailID"); + bulkCopy.ColumnMappings.Add("OrderQty", "OrderQty"); + bulkCopy.ColumnMappings.Add("ProductID", "ProductID"); + bulkCopy.ColumnMappings.Add("UnitPrice", "UnitPrice"); + + // Remove the order hint for the OrderDate column. + bulkCopy.ColumnOrderHints.Remove(hintDate); + + // Write readerDetail to the destination. + try + { + bulkCopy.WriteToServer(readerDetail); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + finally + { + readerDetail.Close(); + } + } + + // Perform a final count on the destination + // tables to see how many rows were added. + long countEndHeader = System.Convert.ToInt32( + countRowHeader.ExecuteScalar()); + Console.WriteLine("{0} rows were added to the Header table.", + countEndHeader - countStartHeader); + long countEndDetail = System.Convert.ToInt32( + countRowDetail.ExecuteScalar()); + Console.WriteLine("{0} rows were added to the Detail table.", + countEndDetail - countStartDetail); + Console.WriteLine("Press Enter to finish."); + Console.ReadLine(); + } + } + } + + private static string GetConnectionString() + // To avoid storing the connection string in your code, + // you can retrieve it from a configuration file. + { + return "Data Source=(local); " + + " Integrated Security=true;" + + "Initial Catalog=AdventureWorks;"; + } +} +// diff --git a/doc/samples/SqlBulkCopy_ColumnOrderHintCollectionRemoveAt.cs b/doc/samples/SqlBulkCopy_ColumnOrderHintCollectionRemoveAt.cs new file mode 100644 index 0000000000..b965599175 --- /dev/null +++ b/doc/samples/SqlBulkCopy_ColumnOrderHintCollectionRemoveAt.cs @@ -0,0 +1,176 @@ +// +using System; +using System.Data; +using Microsoft.Data.SqlClient; + +class Program +{ + static void Main() + { + string connectionString = GetConnectionString(); + // Open a connection to the AdventureWorks database. + using (SqlConnection connection = + new SqlConnection(connectionString)) + { + connection.Open(); + + // Empty the destination tables. + SqlCommand deleteHeader = new SqlCommand( + "DELETE FROM dbo.BulkCopyDemoOrderHeader;", + connection); + deleteHeader.ExecuteNonQuery(); + SqlCommand deleteDetail = new SqlCommand( + "DELETE FROM dbo.BulkCopyDemoOrderDetail;", + connection); + deleteDetail.ExecuteNonQuery(); + + // Perform an initial count on the destination + // table with matching columns. + SqlCommand countRowHeader = new SqlCommand( + "SELECT COUNT(*) FROM dbo.BulkCopyDemoOrderHeader;", + connection); + long countStartHeader = System.Convert.ToInt32( + countRowHeader.ExecuteScalar()); + Console.WriteLine( + "Starting row count for Header table = {0}", + countStartHeader); + + // Perform an initial count on the destination + // table with different column positions. + SqlCommand countRowDetail = new SqlCommand( + "SELECT COUNT(*) FROM dbo.BulkCopyDemoOrderDetail;", + connection); + long countStartDetail = System.Convert.ToInt32( + countRowDetail.ExecuteScalar()); + Console.WriteLine( + "Starting row count for Detail table = {0}", + countStartDetail); + + // Get data from the source table as a SqlDataReader. + // The Sales.SalesOrderHeader and Sales.SalesOrderDetail + // tables are quite large and could easily cause a timeout + // if all data from the tables is added to the destination. + // To keep the example simple and quick, a parameter is + // used to select only orders for a particular account + // as the source for the bulk insert. + SqlCommand headerData = new SqlCommand( + "SELECT [SalesOrderID], [OrderDate], " + + "[AccountNumber] FROM [Sales].[SalesOrderHeader] " + + "WHERE [AccountNumber] = @accountNumber;", + connection); + SqlParameter parameterAccount = new SqlParameter(); + parameterAccount.ParameterName = "@accountNumber"; + parameterAccount.SqlDbType = SqlDbType.NVarChar; + parameterAccount.Direction = ParameterDirection.Input; + parameterAccount.Value = "10-4020-000034"; + headerData.Parameters.Add(parameterAccount); + SqlDataReader readerHeader = headerData.ExecuteReader(); + + // Get the Detail data in a separate connection. + using (SqlConnection connection2 = new SqlConnection(connectionString)) + { + connection2.Open(); + SqlCommand sourceDetailData = new SqlCommand( + "SELECT [Sales].[SalesOrderDetail].[SalesOrderID], [SalesOrderDetailID], " + + "[OrderQty], [ProductID], [UnitPrice] FROM [Sales].[SalesOrderDetail] " + + "INNER JOIN [Sales].[SalesOrderHeader] ON [Sales].[SalesOrderDetail]." + + "[SalesOrderID] = [Sales].[SalesOrderHeader].[SalesOrderID] " + + "WHERE [AccountNumber] = @accountNumber;", connection2); + + SqlParameter accountDetail = new SqlParameter(); + accountDetail.ParameterName = "@accountNumber"; + accountDetail.SqlDbType = SqlDbType.NVarChar; + accountDetail.Direction = ParameterDirection.Input; + accountDetail.Value = "10-4020-000034"; + sourceDetailData.Parameters.Add(accountDetail); + SqlDataReader readerDetail = sourceDetailData.ExecuteReader(); + + // Create the SqlBulkCopy object. + using (SqlBulkCopy bulkCopy = + new SqlBulkCopy(connectionString)) + { + bulkCopy.DestinationTableName = + "dbo.BulkCopyDemoOrderHeader"; + + // Guarantee that columns are mapped correctly by + // defining the column mappings for the order. + bulkCopy.ColumnMappings.Add("SalesOrderID", "SalesOrderID"); + bulkCopy.ColumnMappings.Add("OrderDate", "OrderDate"); + bulkCopy.ColumnMappings.Add("AccountNumber", "AccountNumber"); + + // Add the order hint for the OrderDate column. + SqlBulkCopyColumnOrderHint hintDate = + new SqlBulkCopyColumnOrderHint("OrderDate", SortOrder.Ascending); + bulkCopy.ColumnOrderHints.Add(hintDate); + + // Write readerHeader to the destination. + try + { + bulkCopy.WriteToServer(readerHeader); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + finally + { + readerHeader.Close(); + } + + // Set up the order details destination. + bulkCopy.DestinationTableName = "dbo.BulkCopyDemoOrderDetail"; + + // Clear the ColumnMappingCollection. + bulkCopy.ColumnMappings.Clear(); + + // Add order detail column mappings. + bulkCopy.ColumnMappings.Add("SalesOrderID", "SalesOrderID"); + bulkCopy.ColumnMappings.Add("SalesOrderDetailID", "SalesOrderDetailID"); + bulkCopy.ColumnMappings.Add("OrderQty", "OrderQty"); + bulkCopy.ColumnMappings.Add("ProductID", "ProductID"); + bulkCopy.ColumnMappings.Add("UnitPrice", "UnitPrice"); + + // Remove the order hint for the OrderDate column. + bulkCopy.ColumnOrderHints.RemoveAt(0); + + // Write readerDetail to the destination. + try + { + bulkCopy.WriteToServer(readerDetail); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + finally + { + readerDetail.Close(); + } + } + + // Perform a final count on the destination + // tables to see how many rows were added. + long countEndHeader = System.Convert.ToInt32( + countRowHeader.ExecuteScalar()); + Console.WriteLine("{0} rows were added to the Header table.", + countEndHeader - countStartHeader); + long countEndDetail = System.Convert.ToInt32( + countRowDetail.ExecuteScalar()); + Console.WriteLine("{0} rows were added to the Detail table.", + countEndDetail - countStartDetail); + Console.WriteLine("Press Enter to finish."); + Console.ReadLine(); + } + } + } + + private static string GetConnectionString() + // To avoid storing the connection string in your code, + // you can retrieve it from a configuration file. + { + return "Data Source=(local); " + + " Integrated Security=true;" + + "Initial Catalog=AdventureWorks;"; + } +} +// diff --git a/doc/samples/SqlBulkCopy_ColumnOrderHintColumn.cs b/doc/samples/SqlBulkCopy_ColumnOrderHintColumn.cs new file mode 100644 index 0000000000..6a75e0289c --- /dev/null +++ b/doc/samples/SqlBulkCopy_ColumnOrderHintColumn.cs @@ -0,0 +1,85 @@ +// +using System; +using System.Data; +using Microsoft.Data.SqlClient; + +class Program +{ + static void Main() + { + string connectionString = GetConnectionString(); + // Open a sourceConnection to the AdventureWorks database. + using (SqlConnection sourceConnection = + new SqlConnection(connectionString)) + { + sourceConnection.Open(); + + // Perform an initial count on the destination table. + SqlCommand commandRowCount = new SqlCommand( + "SELECT COUNT(*) FROM " + + "dbo.BulkCopyDemoMatchingColumns;", + sourceConnection); + long countStart = System.Convert.ToInt32( + commandRowCount.ExecuteScalar()); + Console.WriteLine("Starting row count = {0}", countStart); + + // Get data from the source table as a SqlDataReader. + SqlCommand commandSourceData = new SqlCommand( + "SELECT ProductID, Name, " + + "ProductNumber " + + "FROM Production.Product;", sourceConnection); + SqlDataReader reader = + commandSourceData.ExecuteReader(); + + // Set up the bulk copy object. + using (SqlBulkCopy bulkCopy = + new SqlBulkCopy(connectionString)) + { + bulkCopy.DestinationTableName = + "dbo.BulkCopyDemoMatchingColumns"; + + // Setup an order hint for the ProductNumber column. + SqlBulkCopyColumnOrderHint hintNumber = + new SqlBulkCopyColumnOrderHint("number", SortOrder.Ascending); + hintNumber.Column = "ProductNumber"; + bulkCopy.ColumnOrderHints.Add(hintNumber); + + // Write from the source to the destination. + try + { + bulkCopy.WriteToServer(reader); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + finally + { + // Close the SqlDataReader. The SqlBulkCopy + // object is automatically closed at the end + // of the using block. + reader.Close(); + } + } + + // Perform a final count on the destination + // table to see how many rows were added. + long countEnd = System.Convert.ToInt32( + commandRowCount.ExecuteScalar()); + Console.WriteLine("Ending row count = {0}", countEnd); + Console.WriteLine("{0} rows were added.", countEnd - countStart); + Console.WriteLine("Press Enter to finish."); + Console.ReadLine(); + } + } + + private static string GetConnectionString() + // To avoid storing the sourceConnection string in your code, + // you can retrieve it from a configuration file. + { + return "Data Source=(local); " + + " Integrated Security=true;" + + "Initial Catalog=AdventureWorks;"; + } +} +// diff --git a/doc/samples/SqlBulkCopy_ColumnOrderHintSortOrder.cs b/doc/samples/SqlBulkCopy_ColumnOrderHintSortOrder.cs new file mode 100644 index 0000000000..54cef5da53 --- /dev/null +++ b/doc/samples/SqlBulkCopy_ColumnOrderHintSortOrder.cs @@ -0,0 +1,85 @@ +// +using System; +using System.Data; +using Microsoft.Data.SqlClient; + +class Program +{ + static void Main() + { + string connectionString = GetConnectionString(); + // Open a sourceConnection to the AdventureWorks database. + using (SqlConnection sourceConnection = + new SqlConnection(connectionString)) + { + sourceConnection.Open(); + + // Perform an initial count on the destination table. + SqlCommand commandRowCount = new SqlCommand( + "SELECT COUNT(*) FROM " + + "dbo.BulkCopyDemoMatchingColumns;", + sourceConnection); + long countStart = System.Convert.ToInt32( + commandRowCount.ExecuteScalar()); + Console.WriteLine("Starting row count = {0}", countStart); + + // Get data from the source table as a SqlDataReader. + SqlCommand commandSourceData = new SqlCommand( + "SELECT ProductID, Name, " + + "ProductNumber " + + "FROM Production.Product;", sourceConnection); + SqlDataReader reader = + commandSourceData.ExecuteReader(); + + // Set up the bulk copy object. + using (SqlBulkCopy bulkCopy = + new SqlBulkCopy(connectionString)) + { + bulkCopy.DestinationTableName = + "dbo.BulkCopyDemoMatchingColumns"; + + // Setup an order hint for the ProductNumber column. + SqlBulkCopyColumnOrderHint hintNumber = + new SqlBulkCopyColumnOrderHint("ProductNumber", SortOrder.Ascending); + hintNumber.SortOrder = SortOrder.Descending; + bulkCopy.ColumnOrderHints.Add(hintNumber); + + // Write from the source to the destination. + try + { + bulkCopy.WriteToServer(reader); + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + finally + { + // Close the SqlDataReader. The SqlBulkCopy + // object is automatically closed at the end + // of the using block. + reader.Close(); + } + } + + // Perform a final count on the destination + // table to see how many rows were added. + long countEnd = System.Convert.ToInt32( + commandRowCount.ExecuteScalar()); + Console.WriteLine("Ending row count = {0}", countEnd); + Console.WriteLine("{0} rows were added.", countEnd - countStart); + Console.WriteLine("Press Enter to finish."); + Console.ReadLine(); + } + } + + private static string GetConnectionString() + // To avoid storing the sourceConnection string in your code, + // you can retrieve it from a configuration file. + { + return "Data Source=(local); " + + " Integrated Security=true;" + + "Initial Catalog=AdventureWorks;"; + } +} +// diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopy.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopy.xml index 70a502c825..08e804ad7a 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopy.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopy.xml @@ -360,6 +360,24 @@ During the execution of a bulk copy operation, this collection can be accessed, ]]> + + + Returns a collection of + + items. Column order hints describe the sort order of columns in the clustered index of the destination table. + + + A collection of column order hints. By default, it is an empty collection. + + + + + Name of the destination table on the server. @@ -530,6 +548,11 @@ the object's `Finalize` method. To be added. + + A + + did not specify a valid destination column name. + @@ -572,6 +595,11 @@ Transact-SQL `INSERT … SELECT` statement to copy the data. [!code-csharp[SqlBulkCopy.ConnectionString#1](~/../sqlclient/doc/samples/SqlBulkCopy_ConnectionString.cs#1)] ]]> + + A + + did not specify a valid destination column name. + @@ -610,6 +638,11 @@ This code is provided to demonstrate the syntax for using **SqlBulkCopy** only. [!code-csharp[SqlBulkCopy.DataTable#1](~/../sqlclient/doc/samples/SqlBulkCopy_DataTable.cs#1)] ]]> + + A + + did not specify a valid destination column name. + Performing Bulk Copy Operations @@ -664,6 +697,11 @@ The method is calle [!code-csharp[SqlBulkCopy.DataRowState#1](~/../sqlclient/doc/samples/SqlBulkCopy_DataRowState.cs#1)] ]]> + + A + + did not specify a valid destination column name. + An array of objects that will be copied to the destination table. @@ -688,6 +726,11 @@ In this example, a is created at run time. A single ]]> + + A + + did not specify a valid destination column name. + @@ -740,6 +783,10 @@ For more information about asynchronous programming in the .NET Framework Data P object is closed before method execution. is specified in the connection string. + + A + + did not specify a valid destination column name. Returned in the task object, any error returned by SQL Server that occurred while opening the connection. @@ -797,6 +844,9 @@ For more information about asynchronous programming in the .NET Framework Data P object is closed before method execution. is specified in the connection string. + A + + did not specify a valid destination column name. Returned in the task object, any error returned by SQL Server that occurred while opening the connection. @@ -857,6 +907,9 @@ For more information about asynchronous programming in the .NET Framework Data P returned. is specified in the connection string. + A + + did not specify a valid destination column name. Returned in the task object, any error returned by SQL Server that occurred while opening the connection. @@ -960,6 +1013,10 @@ For more information about asynchronous programming in the .NET Framework Data P returned. is specified in the connection string. + + A + + did not specify a valid destination column name. Returned in the task object, any error returned by SQL Server that occurred while opening the connection. @@ -1037,6 +1094,10 @@ For more information about asynchronous programming in the .NET Framework Data P returned. is specified in the connection string. + + A + + did not specify a valid destination column name. Returned in the task object, any error returned by SQL Server that occurred while opening the connection. @@ -1094,8 +1155,12 @@ For more information about asynchronous programming in the .NET Framework Data P object is closed before method execution. is specified in the connection string. + + A + + did not specify a valid destination column name. - + Returned in the task object, any error returned by SQL Server that occurred while opening the connection. @@ -1160,6 +1225,10 @@ For more information about asynchronous programming in the .NET Framework Data P object is closed before method execution. is specified in the connection string. + + A + + did not specify a valid destination column name. Returned in the task object, any error returned by SQL Server that occurred while opening the connection. @@ -1219,6 +1288,10 @@ For more information about asynchronous programming in the .NET Framework Data P object is closed before method execution. is specified in the connection string. + + A + + did not specify a valid destination column name. Returned in the task object, any error returned by SQL Server that occurred while opening the connection. @@ -1291,6 +1364,10 @@ For more information about asynchronous programming in the .NET Framework Data P object is closed before method execution. is specified in the connection string. + + A + + did not specify a valid destination column name. Returned in the task object, any error returned by SQL Server that occurred while opening the connection. diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopyColumnOrderHint.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopyColumnOrderHint.xml new file mode 100644 index 0000000000..8a7b04cf68 --- /dev/null +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopyColumnOrderHint.xml @@ -0,0 +1,131 @@ + + + + + Defines the sort order for a column in a + + instance's destination table, according to the clustered index on the table. + + + collection is not empty, order hints can only be provided for valid +destination columns which have been mapped. + +If a of Unspecified is given, an will be thrown. + +## Examples +The following example bulk copies data from a source table in the **AdventureWorks** sample database to a destination table in the same database. +A SqlBulkCopyColumnOrderHint object is used to define the sort order for the ProductNumber destination column. + +> [!IMPORTANT] +> This sample will not run unless you have created the work tables as described in [Bulk Copy Example Setup](~/docs/framework/data/adonet/sql/bulk-copy-example-setup.md). +This code is provided to demonstrate the syntax for using **SqlBulkCopy** only. If the source and destination tables are in the same SQL Server instance, it is easier and faster to use a +Transact-SQL `INSERT … SELECT` statement to copy the data. + +[!code-csharp[SqlBulkCopy.ColumnOrderHint#1](~/../sqlclient/doc/samples/SqlBulkCopy_ColumnOrderHint.cs#1)] +]]> + + + + + + The name of the destination column within the destination table. + + + The sort order of the corresponding destination column. + + + Creates a new column order hint for the specified destination column. + + + [!IMPORTANT] +> This sample will not run unless you have created the work tables as described in [Bulk Copy Example Setup](~/docs/framework/data/adonet/sql/bulk-copy-example-setup.md). +This code is provided to demonstrate the syntax for using **SqlBulkCopy** only. If the source and destination tables are in the same SQL Server instance, it is easier and faster to use a +Transact-SQL `INSERT … SELECT` statement to copy the data. + +[!code-csharp[SqlBulkCopy.ColumnOrderHint#1](~/../sqlclient/doc/samples/SqlBulkCopy_ColumnOrderHint.cs#1)] +]]> + + + + + + Name of the destination column in the destination table for which the hint is being provided. + + + The string value of the + + property. + + + will be thrown if a null or empty string is given. + +## Examples +The following example bulk copies data from a source table in the **AdventureWorks** sample database to a destination table in the same database. +A SqlBulkCopyColumnOrderHint object is used to define the sort order for the ProductNumber destination column. + +> [!IMPORTANT] +> This sample will not run unless you have created the work tables as described in [Bulk Copy Example Setup](~/docs/framework/data/adonet/sql/bulk-copy-example-setup.md). +This code is provided to demonstrate the syntax for using **SqlBulkCopy** only. If the source and destination tables are in the same SQL Server instance, it is easier and faster to use a +Transact-SQL `INSERT … SELECT` statement to copy the data. + +[!code-csharp[SqlBulkCopy.ColumnOrderHintColumn#1](~/../sqlclient/doc/samples/SqlBulkCopy_ColumnOrderHintColumn.cs#1)] +]]> + + The value is null or empty. + + + + + The sort order of the destination column in the destination table. + + + The SortOrder value of the + + property. + + + will be thrown if a of Unspecified is given. + +## Examples +The following example bulk copies data from a source table in the **AdventureWorks** sample database to a destination table in the same database. +A SqlBulkCopyColumnOrderHint object is used to define the sort order for the ProductNumber destination column. + +> [!IMPORTANT] +> This sample will not run unless you have created the work tables as described in [Bulk Copy Example Setup](~/docs/framework/data/adonet/sql/bulk-copy-example-setup.md). +This code is provided to demonstrate the syntax for using **SqlBulkCopy** only. If the source and destination tables are in the same SQL Server instance, it is easier and faster to use a +Transact-SQL `INSERT … SELECT` statement to copy the data. + +[!code-csharp[SqlBulkCopy.ColumnOrderHintSortOrder#1](~/../sqlclient/doc/samples/SqlBulkCopy_ColumnOrderHintSortOrder.cs#1)] +]]> + + The sort order cannot be unspecified for a column order hint. + + + diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopyColumnOrderHintCollection.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopyColumnOrderHintCollection.xml new file mode 100644 index 0000000000..af8ba0984f --- /dev/null +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlBulkCopyColumnOrderHintCollection.xml @@ -0,0 +1,226 @@ + + + + Collection of objects that inherits from . + + collection is not empty, order hints can only be provided for valid +destination columns which have been mapped. + +If a of Unspecified is given, an will be thrown. + +## Examples + +The following example bulk copies data from a source table in the **AdventureWorks** sample database to a destination table in the same database. + are added to the for the + object to specify order hints for the bulk copy. + +> [!IMPORTANT] +> This sample will not run unless you have created the work tables as described in [Bulk Copy Example Setup](~/docs/framework/data/adonet/sql/bulk-copy-example-setup.md). +This code is provided to demonstrate the syntax for using **SqlBulkCopy** only. If the source and destination tables are in the same SQL Server instance, it is easier and faster to +use a Transact-SQL `INSERT … SELECT` statement to copy the data. + +[!code-csharp[SqlBulkCopy.ColumnOrderHintCollection#1](~/../sqlclient/doc/samples/SqlBulkCopy_ColumnOrderHintCollection.cs#1)] + +]]> + + + + + The object that describes the order hint to be added to the collection. + Adds the specified order hint to the . + A object. + + [!IMPORTANT] +> This sample will not run unless you have created the work tables as described in [Bulk Copy Example Setup](~/docs/framework/data/adonet/sql/bulk-copy-example-setup.md). +This code is provided to demonstrate the syntax for using **SqlBulkCopy** only. If the source and destination tables are in the same SQL Server instance, +it is easier and faster to use a Transact-SQL `INSERT … SELECT` statement to copy the data. + +[!code-csharp[SqlBulkCopy.ColumnOrderHintCollectionAdd#1](~/../sqlclient/doc/samples/SqlBulkCopy_ColumnOrderHintCollectionAdd.cs)] + +]]> + + + The value is null. + + + + The name of the destination column within the destination table. + The sort order of the corresponding destination column. + Creates a new and adds it to the collection. + A column column order hint. + + by providing the destination column name and its sort order. + +> [!IMPORTANT] +> This sample will not run unless you have created the work tables as described in [Bulk Copy Example Setup](~/docs/framework/data/adonet/sql/bulk-copy-example-setup.md). +This code is provided to demonstrate the syntax for using **SqlBulkCopy** only. If the source and destination tables are in the same SQL Server instance, it is easier and faster to use a +Transact-SQL `INSERT … SELECT` statement to copy the data. + +[!code-csharp[SqlBulkCopy.ColumnOrderHintCollectionAdd2#1](~/../sqlclient/doc/samples/SqlBulkCopy_ColumnOrderHintCollectionAdd2.cs#1)] + +]]> + + + + + Clears the contents of the collection. + + method is most commonly used when you use a single +instance to process more than one bulk copy operation. If you create column order hints for one bulk copy operation, you must clear the + after the method and before processing the next bulk copy. + +Performing several bulk copies using the same instance will usually be more efficient from a performance point of view than using a separate + for each operation. + +## Examples +The following example performs two bulk copy operations. The first operation copies sales order header information, and the second copies sales order details. +The example defines a column order hint for each bulk copy operation. +The method must be used after the first bulk copy is performed and before the next bulk copy's order hint is defined. + +> [!IMPORTANT] +> This sample will not run unless you have created the work tables as described in [Bulk Copy Example Setup](~/docs/framework/data/adonet/sql/bulk-copy-example-setup.md). +This code is provided to demonstrate the syntax for using **SqlBulkCopy** only. If the source and destination tables are in the same SQL Server instance, it is easier and faster to use a +Transact-SQL `INSERT … SELECT` statement to copy the data. + +[!code-csharp[SqlBulkCopy.ColumnOrderHintCollectionClear#1](~/../sqlclient/doc/samples/SqlBulkCopy_ColumnOrderHintCollectionClear.cs#1)] + +]]> + + + + A valid object. + Gets a value indicating whether a specified object exists in the collection. + + if the specified column order hint exists in the collection; otherwise . + + + The one-dimensional array that is the destination of the elements copied from + . The array must have zero-based indexing. + The zero-based index in at which copying begins. + Copies the elements of the to an array of + items, starting at a particular index. + + + + The object for which to search. + Gets the index of the specified object. + The zero-based index of the column order hint, or -1 if the column order hint is not found in the collection. + To be added. + + + Integer value of the location within the at which to insert the new + . + + object to be inserted in the collection. + Insert a new at the index specified. + The order in which column order hints can be added is arbitrary. + The index is less than zero or greater than . + A null column order hint cannot be added to the collection. + + + The zero-based index of the to find. + Gets the object at the specified index. + A object. + To be added. + The index must be non-negative and less than the size of the collection. + + + + object to be removed from the collection. + Removes the specified element from the . + + method is most commonly used when you use a single +instance to process more than one bulk copy operation. If you create column order hints for one bulk copy operation, you must clear the + after the method and before processing the next bulk copy. +You can clear the entire collection by using the + method, or remove hints individually using the +method or the method. + +Performing several bulk copies using the same instance will usually be more efficient from a performance point of view than using a separate + for each operation. + +## Examples +The following example performs two bulk copy operations. The first operation copies sales order header information, and the second copies sales order details. +The example defines a column order hint for the **OrderDate** column in the first bulk copy operation. The hint is removed before the second bulk copy operation. + +> [!IMPORTANT] +> This sample will not run unless you have created the work tables as described in [Bulk Copy Example Setup](~/docs/framework/data/adonet/sql/bulk-copy-example-setup.md). +This code is provided to demonstrate the syntax for using **SqlBulkCopy** only. If the source and destination tables are in the same SQL Server instance, it is easier and faster to use a +Transact-SQL `INSERT … SELECT` statement to copy the data. + +[!code-csharp[SqlBulkCopy.ColumnOrderHintCollectionRemove#1](~/../sqlclient/doc/samples/SqlBulkCopy_ColumnOrderHintCollectionRemove.cs#1)] + +]]> + + The value is null. + + + The zero-based index of the object to be removed from the collection. + Removes the column order hint at the specified index from the collection. + + method is most commonly used when you use a single +instance to process more than one bulk copy operation. If you create column order hints for one bulk copy operation, you must clear the + after the method and before processing the next bulk copy. +You can clear the entire collection by using the + method, or remove hints individually using the +method or the method. + +Performing several bulk copies using the same instance will usually be more efficient from a performance point of view than using a separate + for each operation. + +## Examples +The following example performs two bulk copy operations. The first operation copies sales order header information, and the second copies sales order details. +The example defines a column order hint for the **OrderDate** column in the first bulk copy operation. The hint is removed before the second bulk copy operation. + +> [!IMPORTANT] +> This sample will not run unless you have created the work tables as described in [Bulk Copy Example Setup](~/docs/framework/data/adonet/sql/bulk-copy-example-setup.md). +This code is provided to demonstrate the syntax for using **SqlBulkCopy** only. If the source and destination tables are in the same SQL Server instance, it is easier and faster to use a +Transact-SQL `INSERT … SELECT` statement to copy the data. + +[!code-csharp[SqlBulkCopy.ColumnOrderHintCollectionRemoveAt#1](~/../sqlclient/doc/samples/SqlBulkCopy_ColumnOrderHintCollectionRemoveAt.cs#1)] + +]]> + + The index must be non-negative and less than the size of the collection. + + + Gets a value indicating whether access to the is synchronized (thread safe). + `true` if access to the is synchronized (thread safe); otherwise, `false`. + + + diff --git a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs index b3b5097ed6..52e6e35dd3 100644 --- a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs @@ -143,6 +143,8 @@ public sealed partial class SqlBulkCopy : System.IDisposable public bool EnableStreaming { get { throw null; } set { } } /// public Microsoft.Data.SqlClient.SqlBulkCopyColumnMappingCollection ColumnMappings { get { throw null; } } + /// + public SqlBulkCopyColumnOrderHintCollection ColumnOrderHints { get { throw null; } } /// public string DestinationTableName { get { throw null; } set { } } /// @@ -239,6 +241,40 @@ public sealed partial class SqlBulkCopyColumnMappingCollection : System.Collecti /// public new void RemoveAt(int index) { } } + /// + public sealed class SqlBulkCopyColumnOrderHint + { + /// + public SqlBulkCopyColumnOrderHint(string column, SortOrder sortOrder) { } + /// + public string Column { get { throw null; } set { } } + /// + public SortOrder SortOrder { get { throw null; } set { } } + } + /// + public sealed class SqlBulkCopyColumnOrderHintCollection : System.Collections.CollectionBase + { + /// + public SqlBulkCopyColumnOrderHint this[int index] { get { throw null; } } + /// + public SqlBulkCopyColumnOrderHint Add(SqlBulkCopyColumnOrderHint columnOrderHint) { throw null; } + /// + public SqlBulkCopyColumnOrderHint Add(string column, SortOrder sortOrder) { throw null; } + /// + public new void Clear() { } + /// + public bool Contains(SqlBulkCopyColumnOrderHint value) { throw null; } + /// + public void CopyTo(SqlBulkCopyColumnOrderHint[] array, int index) { } + /// + public int IndexOf(SqlBulkCopyColumnOrderHint value) { throw null; } + /// + public void Insert(int index, SqlBulkCopyColumnOrderHint columnOrderHint) { } + /// + public void Remove(SqlBulkCopyColumnOrderHint columnOrderHint) { } + /// + public new void RemoveAt(int index) { } + } /// [System.FlagsAttribute] public enum SqlBulkCopyOptions 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 41c3596704..c31298fb12 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft.Data.SqlClient.csproj @@ -96,6 +96,12 @@ Microsoft\Data\SqlClient\SqlBulkCopyOptions.cs + + Microsoft\Data\SqlClient\SqlBulkCopyColumnOrderHint.cs + + + Microsoft\Data\SqlClient\SqlBulkCopyColumnOrderHintCollection.cs + Microsoft\Data\SqlClient\SqlClientEncryptionAlgorithmFactory.cs diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs index 686e194397..adfc7ec37c 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs @@ -218,13 +218,13 @@ private int RowNumber rowNo = ((DataTable)_dataTableSource).Rows.IndexOf(_rowEnumerator.Current as DataRow); break; case ValueSourceType.DataTable: - rowNo = ((DataTable)_rowSource).Rows.IndexOf(_rowEnumerator.Current as DataRow); + rowNo = ((DataTable)_rowSource).Rows.IndexOf(_rowEnumerator.Current as DataRow); break; case ValueSourceType.DbDataReader: case ValueSourceType.IDataReader: case ValueSourceType.Unspecified: default: - return -1; + return -1; } return ++rowNo; } @@ -272,6 +272,7 @@ public SqlBulkCopy(SqlConnection connection) } _connection = connection; _columnMappings = new SqlBulkCopyColumnMappingCollection(); + ColumnOrderHints = new SqlBulkCopyColumnOrderHintCollection(); } /// @@ -299,6 +300,7 @@ public SqlBulkCopy(string connectionString) } _connection = new SqlConnection(connectionString); _columnMappings = new SqlBulkCopyColumnMappingCollection(); + ColumnOrderHints = new SqlBulkCopyColumnOrderHintCollection(); _ownConnection = true; } @@ -368,6 +370,12 @@ public SqlBulkCopyColumnMappingCollection ColumnMappings } } + /// + public SqlBulkCopyColumnOrderHintCollection ColumnOrderHints + { + get; + } + /// public string DestinationTableName { @@ -604,6 +612,8 @@ private string AnalyzeTargetAndCreateUpdateBulkCommand(BulkCopySimpleResultSet i throw SQL.BulkLoadExistingTransaction(); } + HashSet destColumnNames = new HashSet(); + // Loop over the metadata for each column _SqlMetaDataSet metaDataSet = internalResults[MetaDataResultId].MetaData; _sortedColumnMappings = new List<_ColumnMapping>(metaDataSet.Length); @@ -636,6 +646,7 @@ private string AnalyzeTargetAndCreateUpdateBulkCommand(BulkCopySimpleResultSet i } _sortedColumnMappings.Add(new _ColumnMapping(_localColumnMappings[assocId]._internalSourceColumnOrdinal, metadata)); + destColumnNames.Add(metadata.column); nmatched++; if (nmatched > 1) @@ -770,12 +781,13 @@ private string AnalyzeTargetAndCreateUpdateBulkCommand(BulkCopySimpleResultSet i updateBulkCommandText.Append(")"); - if ((_copyOptions & ( + if (((_copyOptions & ( SqlBulkCopyOptions.KeepNulls | SqlBulkCopyOptions.TableLock | SqlBulkCopyOptions.CheckConstraints | SqlBulkCopyOptions.FireTriggers | SqlBulkCopyOptions.AllowEncryptedValueModifications)) != SqlBulkCopyOptions.Default) + || ColumnOrderHints.Count > 0) { bool addSeparator = false; // Insert a comma character if multiple options in list updateBulkCommandText.Append(" with ("); @@ -804,11 +816,40 @@ private string AnalyzeTargetAndCreateUpdateBulkCommand(BulkCopySimpleResultSet i updateBulkCommandText.Append((addSeparator ? ", " : "") + "ALLOW_ENCRYPTED_VALUE_MODIFICATIONS"); addSeparator = true; } + if (ColumnOrderHints.Count > 0) + { + updateBulkCommandText.Append((addSeparator ? ", " : "") + TryGetOrderHintText(destColumnNames)); + } updateBulkCommandText.Append(")"); } return (updateBulkCommandText.ToString()); } + private string TryGetOrderHintText(HashSet destColumnNames) + { + StringBuilder orderHintText = new StringBuilder("ORDER("); + + foreach (SqlBulkCopyColumnOrderHint orderHint in ColumnOrderHints) + { + string columnNameArg = orderHint.Column; + if (!destColumnNames.Contains(columnNameArg)) + { + // column is not valid in the destination table + throw SQL.BulkLoadOrderHintInvalidColumn(columnNameArg); + } + if (!string.IsNullOrEmpty(columnNameArg)) + { + string columnNameEscaped = SqlServerEscapeHelper.EscapeIdentifier(SqlServerEscapeHelper.EscapeStringAsLiteral(columnNameArg)); + string sortOrderText = orderHint.SortOrder == SortOrder.Descending ? "DESC" : "ASC"; + orderHintText.Append($"{columnNameEscaped} {sortOrderText}, "); + } + } + + orderHintText.Length = orderHintText.Length - 2; + orderHintText.Append(")"); + return orderHintText.ToString(); + } + private Task SubmitUpdateBulkCommand(string TDSCommand) { SqlClientEventSource.Log.CorrelationTraceEvent(" ObjectID{0}, ActivityID {1}", ObjectID, ActivityCorrelator.Current); 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 0905a3ba0f..5c8ec93400 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 @@ -871,6 +871,26 @@ internal static Exception BulkLoadNonMatchingColumnName(string columnName, Excep { return ADP.InvalidOperation(System.SRHelper.GetString(SR.SQL_BulkLoadNonMatchingColumnName, columnName), e); } + internal static Exception BulkLoadNullEmptyColumnName(string paramName) + { + return ADP.Argument(string.Format(System.SRHelper.GetString(SR.SQL_ParameterCannotBeEmpty), paramName)); + } + internal static Exception BulkLoadUnspecifiedSortOrder() + { + return ADP.Argument(System.SRHelper.GetString(SR.SQL_BulkLoadUnspecifiedSortOrder)); + } + internal static Exception BulkLoadInvalidOrderHint() + { + return ADP.Argument(System.SRHelper.GetString(SR.SQL_BulkLoadInvalidOrderHint)); + } + internal static Exception BulkLoadOrderHintInvalidColumn(string columnName) + { + return ADP.InvalidOperation(string.Format(System.SRHelper.GetString(SR.SQL_BulkLoadOrderHintInvalidColumn), columnName)); + } + internal static Exception BulkLoadOrderHintDuplicateColumn(string columnName) + { + return ADP.InvalidOperation(string.Format(System.SRHelper.GetString(SR.SQL_BulkLoadOrderHintDuplicateColumn), columnName)); + } internal static Exception BulkLoadStringTooLong(string tableName, string columnName, string truncatedValue) { return ADP.InvalidOperation(System.SRHelper.GetString(SR.SQL_BulkLoadStringTooLong, tableName, columnName, truncatedValue)); diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.Designer.cs b/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.Designer.cs index 10848064a8..fd5175504c 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.Designer.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.Designer.cs @@ -2193,6 +2193,15 @@ internal class SR { } } + /// + /// Looks up a localized string similar to The given column order hint is not valid.. + /// + internal static string SQL_BulkLoadInvalidOrderHint { + get { + return ResourceManager.GetString("SQL_BulkLoadInvalidOrderHint", resourceCulture); + } + } + /// /// Looks up a localized string similar to Timeout Value '{0}' is less than 0.. /// @@ -2283,6 +2292,24 @@ internal class SR { } } + /// + /// Looks up a localized string similar to The column '{0}' was specified more than once.. + /// + internal static string SQL_BulkLoadOrderHintDuplicateColumn { + get { + return ResourceManager.GetString("SQL_BulkLoadOrderHintDuplicateColumn", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The sorted column '{0}' is not valid in the destination table.. + /// + internal static string SQL_BulkLoadOrderHintInvalidColumn { + get { + return ResourceManager.GetString("SQL_BulkLoadOrderHintInvalidColumn", resourceCulture); + } + } + /// /// Looks up a localized string similar to Attempt to invoke bulk copy on an object that has a pending operation.. /// @@ -2301,6 +2328,15 @@ internal class SR { } } + /// + /// Looks up a localized string similar to A column order hint cannot have an unspecified sort order.. + /// + internal static string SQL_BulkLoadUnspecifiedSortOrder { + get { + return ResourceManager.GetString("SQL_BulkLoadUnspecifiedSortOrder", resourceCulture); + } + } + /// /// Looks up a localized string similar to Failed to instantiate a SqlAuthenticationInitializer with type '{0}'.. /// diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.resx b/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.resx index 18a556eb63..bab71b9600 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.resx +++ b/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.resx @@ -1860,6 +1860,18 @@ The given value{0} of type {1} from the data source cannot be converted to type {2} for Column {3} [{4}]. + + A column order hint cannot have an unspecified sort order. + + + The given column order hint is not valid. + + + The column '{0}' was specified more than once. + + + The sorted column '{0}' is not valid in the destination table. + Cannot set the Credential property if 'Authentication=Active Directory Integrated' has been specified in the connection string. @@ -1878,4 +1890,4 @@ Cannot use 'Authentication=Active Directory Interactive', if the Credential property has been set. - \ No newline at end of file + diff --git a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs index 9238acc23f..345ebcea9e 100644 --- a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs @@ -157,6 +157,8 @@ public sealed partial class SqlBulkCopy : System.IDisposable public int BulkCopyTimeout { get { throw null; } set { } } /// public Microsoft.Data.SqlClient.SqlBulkCopyColumnMappingCollection ColumnMappings { get { throw null; } } + /// + public SqlBulkCopyColumnOrderHintCollection ColumnOrderHints { get { throw null; } } /// public string DestinationTableName { get { throw null; } set { } } /// @@ -255,6 +257,40 @@ public sealed partial class SqlBulkCopyColumnMappingCollection : System.Collecti /// public new void RemoveAt(int index) { } } + /// + public sealed class SqlBulkCopyColumnOrderHint + { + /// + public SqlBulkCopyColumnOrderHint(string column, SortOrder sortOrder) { } + /// + public string Column { get { throw null; } set { } } + /// + public SortOrder SortOrder { get { throw null; } set { } } + } + /// + public sealed class SqlBulkCopyColumnOrderHintCollection : System.Collections.CollectionBase + { + /// + public SqlBulkCopyColumnOrderHint this[int index] { get { throw null; } } + /// + public SqlBulkCopyColumnOrderHint Add(SqlBulkCopyColumnOrderHint columnOrderHint) { throw null; } + /// + public SqlBulkCopyColumnOrderHint Add(string column, SortOrder sortOrder) { throw null; } + /// + public new void Clear() { } + /// + public bool Contains(SqlBulkCopyColumnOrderHint value) { throw null; } + /// + public void CopyTo(SqlBulkCopyColumnOrderHint[] array, int index) { } + /// + public int IndexOf(SqlBulkCopyColumnOrderHint value) { throw null; } + /// + public void Insert(int index, SqlBulkCopyColumnOrderHint columnOrderHint) { } + /// + public void Remove(SqlBulkCopyColumnOrderHint columnOrderHint) { } + /// + public new void RemoveAt(int index) { } + } /// [System.FlagsAttribute] public enum SqlBulkCopyOptions 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 b60bf1ae7d..858fadfdb4 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft.Data.SqlClient.csproj @@ -150,6 +150,12 @@ Microsoft\Data\SqlClient\SqlBulkCopyOptions.cs + + Microsoft\Data\SqlClient\SqlBulkCopyColumnOrderHint.cs + + + Microsoft\Data\SqlClient\SqlBulkCopyColumnOrderHintCollection.cs + Microsoft\Data\SqlClient\SqlClientEncryptionAlgorithmFactory.cs diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs index c4c92977d4..37a4ff7c30 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlBulkCopy.cs @@ -333,6 +333,7 @@ public SqlBulkCopy(SqlConnection connection) } _connection = connection; _columnMappings = new SqlBulkCopyColumnMappingCollection(); + ColumnOrderHints = new SqlBulkCopyColumnOrderHintCollection(); } /// @@ -361,6 +362,7 @@ public SqlBulkCopy(string connectionString) : this(new SqlConnection(connectionS } _connection = new SqlConnection(connectionString); _columnMappings = new SqlBulkCopyColumnMappingCollection(); + ColumnOrderHints = new SqlBulkCopyColumnOrderHintCollection(); _ownConnection = true; } @@ -430,6 +432,12 @@ public SqlBulkCopyColumnMappingCollection ColumnMappings } } + /// + public SqlBulkCopyColumnOrderHintCollection ColumnOrderHints + { + get; + } + /// public string DestinationTableName { @@ -692,6 +700,8 @@ private string AnalyzeTargetAndCreateUpdateBulkCommand(BulkCopySimpleResultSet i throw SQL.BulkLoadExistingTransaction(); } + HashSet destColumnNames = new HashSet(); + // loop over the metadata for each column // _SqlMetaDataSet metaDataSet = internalResults[MetaDataResultId].MetaData; @@ -726,6 +736,7 @@ private string AnalyzeTargetAndCreateUpdateBulkCommand(BulkCopySimpleResultSet i } _sortedColumnMappings.Add(new _ColumnMapping(_localColumnMappings[assocId]._internalSourceColumnOrdinal, metadata)); + destColumnNames.Add(metadata.column); nmatched++; if (nmatched > 1) @@ -872,12 +883,13 @@ private string AnalyzeTargetAndCreateUpdateBulkCommand(BulkCopySimpleResultSet i updateBulkCommandText.Append(")"); - if ((_copyOptions & ( + if (((_copyOptions & ( SqlBulkCopyOptions.KeepNulls | SqlBulkCopyOptions.TableLock | SqlBulkCopyOptions.CheckConstraints | SqlBulkCopyOptions.FireTriggers | SqlBulkCopyOptions.AllowEncryptedValueModifications)) != SqlBulkCopyOptions.Default) + || ColumnOrderHints.Count > 0) { bool addSeparator = false; // insert a comma character if multiple options in list ... updateBulkCommandText.Append(" with ("); @@ -906,11 +918,40 @@ private string AnalyzeTargetAndCreateUpdateBulkCommand(BulkCopySimpleResultSet i updateBulkCommandText.Append((addSeparator ? ", " : "") + "ALLOW_ENCRYPTED_VALUE_MODIFICATIONS"); addSeparator = true; } + if (ColumnOrderHints.Count > 0) + { + updateBulkCommandText.Append((addSeparator ? ", " : "") + TryGetOrderHintText(destColumnNames)); + } updateBulkCommandText.Append(")"); } return (updateBulkCommandText.ToString()); } + private string TryGetOrderHintText(HashSet destColumnNames) + { + StringBuilder orderHintText = new StringBuilder("ORDER("); + + foreach (SqlBulkCopyColumnOrderHint orderHint in ColumnOrderHints) + { + string columnNameArg = orderHint.Column; + if (!destColumnNames.Contains(columnNameArg)) + { + // column is not valid in the destination table + throw SQL.BulkLoadOrderHintInvalidColumn(columnNameArg); + } + if (!string.IsNullOrEmpty(columnNameArg)) + { + string columnNameEscaped = SqlServerEscapeHelper.EscapeIdentifier(SqlServerEscapeHelper.EscapeStringAsLiteral(columnNameArg)); + string sortOrderText = orderHint.SortOrder == SortOrder.Descending ? "DESC" : "ASC"; + orderHintText.Append($"{columnNameEscaped} {sortOrderText}, "); + } + } + + orderHintText.Length = orderHintText.Length - 2; + orderHintText.Append(")"); + return orderHintText.ToString(); + } + // submitts the updatebulk command // private Task SubmitUpdateBulkCommand(string TDSCommand) @@ -2934,18 +2975,18 @@ private void CopyBatchesAsyncContinuedOnError(bool cleanupParser) { tdsReliabilitySection.Start(); #endif //DEBUG - if ((cleanupParser) && (_parser != null) && (_stateObj != null)) - { - _parser._asyncWrite = false; - Task task = _parser.WriteBulkCopyDone(_stateObj); - Debug.Assert(task == null, "Write should not pend when error occurs"); - RunParser(); - } + if ((cleanupParser) && (_parser != null) && (_stateObj != null)) + { + _parser._asyncWrite = false; + Task task = _parser.WriteBulkCopyDone(_stateObj); + Debug.Assert(task == null, "Write should not pend when error occurs"); + RunParser(); + } - if (_stateObj != null) - { - CleanUpStateObject(); - } + if (_stateObj != null) + { + CleanUpStateObject(); + } #if DEBUG } finally 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 49bec90d25..6cb5b7ed8c 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 @@ -1015,6 +1015,26 @@ static internal Exception BulkLoadNonMatchingColumnName(string columnName, Excep { return ADP.InvalidOperation(StringsHelper.GetString(Strings.SQL_BulkLoadNonMatchingColumnName, columnName), e); } + internal static Exception BulkLoadNullEmptyColumnName(string paramName) + { + return ADP.Argument(string.Format(StringsHelper.GetString(Strings.SQL_ParameterCannotBeEmpty), paramName)); + } + internal static Exception BulkLoadUnspecifiedSortOrder() + { + return ADP.Argument(StringsHelper.GetString(Strings.SQL_BulkLoadUnspecifiedSortOrder)); + } + internal static Exception BulkLoadInvalidOrderHint() + { + return ADP.Argument(StringsHelper.GetString(Strings.SQL_BulkLoadInvalidOrderHint)); + } + internal static Exception BulkLoadOrderHintInvalidColumn(string columnName) + { + return ADP.InvalidOperation(string.Format(StringsHelper.GetString(Strings.SQL_BulkLoadOrderHintInvalidColumn), columnName)); + } + internal static Exception BulkLoadOrderHintDuplicateColumn(string columnName) + { + return ADP.InvalidOperation(string.Format(StringsHelper.GetString(Strings.SQL_BulkLoadOrderHintDuplicateColumn), columnName)); + } static internal Exception BulkLoadStringTooLong(string tableName, string columnName, string truncatedValue) { return ADP.InvalidOperation(StringsHelper.GetString(Strings.SQL_BulkLoadStringTooLong, tableName, columnName, truncatedValue)); diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs index 2d0d237f75..9f40b57bdd 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs @@ -8910,6 +8910,15 @@ internal class Strings { } } + /// + /// Looks up a localized string similar to The given column order hint is not valid.. + /// + internal static string SQL_BulkLoadInvalidOrderHint { + get { + return ResourceManager.GetString("SQL_BulkLoadInvalidOrderHint", resourceCulture); + } + } + /// /// Looks up a localized string similar to Timeout Value '{0}' is less than 0.. /// @@ -9000,6 +9009,24 @@ internal class Strings { } } + /// + /// Looks up a localized string similar to The column '{0}' was specified more than once.. + /// + internal static string SQL_BulkLoadOrderHintDuplicateColumn { + get { + return ResourceManager.GetString("SQL_BulkLoadOrderHintDuplicateColumn", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The sorted column '{0}' is not valid in the destination table.. + /// + internal static string SQL_BulkLoadOrderHintInvalidColumn { + get { + return ResourceManager.GetString("SQL_BulkLoadOrderHintInvalidColumn", resourceCulture); + } + } + /// /// Looks up a localized string similar to Attempt to invoke bulk copy on an object that has a pending operation.. /// @@ -9018,6 +9045,15 @@ internal class Strings { } } + /// + /// Looks up a localized string similar to A column order hint cannot have an unspecified sort order.. + /// + internal static string SQL_BulkLoadUnspecifiedSortOrder { + get { + return ResourceManager.GetString("SQL_BulkLoadUnspecifiedSortOrder", resourceCulture); + } + } + /// /// Looks up a localized string similar to Failed to instantiate a SqlAuthenticationInitializer with type '{0}'.. /// diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx index 628471f836..0bca401c21 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx +++ b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx @@ -4530,6 +4530,18 @@ UDT size must be less than {1}, size: {0} + + The given column order hint is not valid. + + + The column '{0}' was specified more than once. + + + The sorted column '{0}' is not valid in the destination table. + + + A column order hint cannot have an unspecified sort order. + Unsupported authentication specified in this context: {0} @@ -4542,4 +4554,4 @@ Cannot set the Credential property if 'Authentication=Active Directory Interactive' has been specified in the connection string. - \ No newline at end of file + diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopyColumnOrderHint.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopyColumnOrderHint.cs new file mode 100644 index 0000000000..2329df4140 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopyColumnOrderHint.cs @@ -0,0 +1,66 @@ +// 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; + +namespace Microsoft.Data.SqlClient +{ + /// + public sealed class SqlBulkCopyColumnOrderHint + { + /// + public SqlBulkCopyColumnOrderHint(string column, SortOrder sortOrder) + { + Column = column; + SortOrder = sortOrder; + } + + private string _columnName; + private SortOrder _sortOrder; + + internal event EventHandler NameChanging; + + /// + public string Column + { + get => _columnName ?? string.Empty; + set + { + if (string.IsNullOrEmpty(value)) + { + throw SQL.BulkLoadNullEmptyColumnName(nameof(Column)); + } + // Do nothing if column name is the same + if (_columnName != value) + { + OnNameChanging(value); + _columnName = value; + } + } + } + + /// + public SortOrder SortOrder + { + get => _sortOrder; + set + { + if (value != SortOrder.Unspecified) + { + _sortOrder = value; + } + else + { + throw SQL.BulkLoadUnspecifiedSortOrder(); + } + } + } + + private void OnNameChanging(string newName) + { + var handler = NameChanging; + handler?.Invoke(this, newName); + } + } +} diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopyColumnOrderHintCollection.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopyColumnOrderHintCollection.cs new file mode 100644 index 0000000000..82fda1ad7d --- /dev/null +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlBulkCopyColumnOrderHintCollection.cs @@ -0,0 +1,133 @@ +// 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.Collections; +using System.Collections.Generic; + +namespace Microsoft.Data.SqlClient +{ + /// + public sealed class SqlBulkCopyColumnOrderHintCollection : CollectionBase + { + private readonly HashSet _columnNames = new HashSet(); + + /// + public SqlBulkCopyColumnOrderHint this[int index] => (SqlBulkCopyColumnOrderHint)List[index]; + + /// + public SqlBulkCopyColumnOrderHint Add(SqlBulkCopyColumnOrderHint columnOrderHint) + { + if (columnOrderHint == null) + { + throw new ArgumentNullException(nameof(columnOrderHint)); + } + if (string.IsNullOrEmpty(columnOrderHint.Column) || + columnOrderHint.SortOrder == SortOrder.Unspecified) + { + throw SQL.BulkLoadInvalidOrderHint(); + } + RegisterColumnName(columnOrderHint, columnOrderHint.Column); + InnerList.Add(columnOrderHint); + return columnOrderHint; + } + + /// + public SqlBulkCopyColumnOrderHint Add(string column, SortOrder sortOrder) => Add(new SqlBulkCopyColumnOrderHint(column, sortOrder)); + + /// + /// Invoked before the collection is cleared using Clear(). Unregisters each order hint. + /// + protected override void OnClear() + { + foreach (SqlBulkCopyColumnOrderHint orderHint in InnerList) + { + UnregisterColumnName(orderHint, orderHint.Column); + } + } + + /// + public bool Contains(SqlBulkCopyColumnOrderHint value) => InnerList.Contains(value); + + /// + public void CopyTo(SqlBulkCopyColumnOrderHint[] array, int index) => InnerList.CopyTo(array, index); + + /// + public int IndexOf(SqlBulkCopyColumnOrderHint value) => InnerList.IndexOf(value); + + /// + public void Insert(int index, SqlBulkCopyColumnOrderHint columnOrderHint) + { + // Try inserting into an invalid index to throw an exception + if (index < 0 || index > InnerList.Count) + { + InnerList.Insert(index, columnOrderHint); + } + if (columnOrderHint == null) + { + throw new ArgumentNullException(nameof(columnOrderHint)); + } + RegisterColumnName(columnOrderHint, columnOrderHint.Column); + InnerList.Insert(index, columnOrderHint); + } + + /// + public void Remove(SqlBulkCopyColumnOrderHint columnOrderHint) + { + if (columnOrderHint == null) + { + throw new ArgumentNullException(nameof(columnOrderHint)); + } + // OnRemove only works with the List instance and not the InnerList instance + List.Remove(columnOrderHint); + } + + /// + /// Invoked before the order hint is removed using Remove() or RemoveAt(). Unregisters the order hint. + /// + protected override void OnRemove(int index, object value) + { + if (value is SqlBulkCopyColumnOrderHint orderHint) + { + UnregisterColumnName(orderHint, orderHint.Column); + } + else + { + throw new ArgumentNullException(nameof(orderHint)); + } + } + + private void ColumnNameChanging(object sender, string newName) + { + if (sender is SqlBulkCopyColumnOrderHint orderHint) + { + if (_columnNames.Contains(newName)) + { + throw SQL.BulkLoadOrderHintDuplicateColumn(newName); + } + UnregisterColumnName(orderHint, orderHint.Column); + RegisterColumnName(orderHint, newName); + } + } + + private void RegisterColumnName(SqlBulkCopyColumnOrderHint orderHint, string columnName) + { + if (_columnNames.Contains(columnName)) + { + throw SQL.BulkLoadOrderHintDuplicateColumn(orderHint.Column); + } + _columnNames.Add(columnName); + orderHint.NameChanging += ColumnNameChanging; + } + + private void UnregisterColumnName(SqlBulkCopyColumnOrderHint orderHint, string columnName) + { + if (Contains(orderHint)) + { + _columnNames.Remove(columnName); + orderHint.NameChanging -= ColumnNameChanging; + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj index 63371aaa78..c0cdf6fd85 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/Microsoft.Data.SqlClient.Tests.csproj @@ -44,6 +44,7 @@ + diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlBulkCopyColumnOrderHintCollectionTest.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlBulkCopyColumnOrderHintCollectionTest.cs new file mode 100644 index 0000000000..b6104335dd --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlBulkCopyColumnOrderHintCollectionTest.cs @@ -0,0 +1,583 @@ +// 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.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using Xunit; + +namespace Microsoft.Data.SqlClient.Tests +{ + public class SqlBulkCopyColumnOrderHintCollectionTest + { + private static SqlBulkCopyColumnOrderHintCollection CreateCollection() => new SqlBulkCopy(new SqlConnection()).ColumnOrderHints; + + private static SqlBulkCopyColumnOrderHintCollection CreateCollection(params SqlBulkCopyColumnOrderHint[] orderHints) + { + Assert.NotNull(orderHints); + + SqlBulkCopyColumnOrderHintCollection collection = CreateCollection(); + + foreach (SqlBulkCopyColumnOrderHint orderHint in orderHints) + { + collection.Add(orderHint); + } + + return collection; + } + + [Fact] + public void Properties_ReturnFalse() + { + IList list = CreateCollection(); + Assert.False(list.IsSynchronized); + Assert.False(list.IsFixedSize); + Assert.False(list.IsReadOnly); + } + + [Fact] + public void Methods_NullParameterPassed_ThrowsArgumentNullException() + { + SqlBulkCopyColumnOrderHintCollection collection = CreateCollection(); + collection.Add(new SqlBulkCopyColumnOrderHint("column", SortOrder.Ascending)); + + Assert.Throws(() => collection.CopyTo(null, 0)); + Assert.Throws(() => collection.Add(null)); + + Assert.Throws(() => collection.Insert(0, null)); + Assert.Throws(() => collection.Remove(null)); + + IList list = collection; + Assert.Throws(() => list[0] = null); + Assert.Throws(() => list.Add(null)); + Assert.Throws(() => list.CopyTo(null, 0)); + Assert.Throws(() => list.Insert(0, null)); + Assert.Throws(() => list.Remove(null)); + } + + [Fact] + public void Members_InvalidRange_ThrowsArgumentOutOfRangeException() + { + SqlBulkCopyColumnOrderHintCollection collection = CreateCollection(); + + var item = new SqlBulkCopyColumnOrderHint("column", SortOrder.Ascending); + + Assert.Throws(() => collection[-1]); + Assert.Throws(() => collection[collection.Count]); + Assert.Throws(() => collection.Insert(-1, item)); + Assert.Throws(() => collection.Insert(collection.Count + 1, item)); + Assert.Throws(() => collection.RemoveAt(-1)); + Assert.Throws(() => collection.RemoveAt(collection.Count)); + + IList list = collection; + Assert.Throws(() => list[-1]); + Assert.Throws(() => list[collection.Count]); + Assert.Throws(() => list[-1] = item); + Assert.Throws(() => list[collection.Count] = item); + Assert.Throws(() => list.Insert(-1, item)); + Assert.Throws(() => list.Insert(collection.Count + 1, item)); + Assert.Throws(() => list.RemoveAt(-1)); + Assert.Throws(() => list.RemoveAt(collection.Count)); + } + + [Fact] + public void Add_AddItems_ItemsAddedAsEpected() + { + SqlBulkCopyColumnOrderHintCollection collection = CreateCollection(); + var item1 = new SqlBulkCopyColumnOrderHint("column1", SortOrder.Ascending); + Assert.Same(item1, collection.Add(item1)); + Assert.Same(item1, collection[0]); + var item2 = new SqlBulkCopyColumnOrderHint("column2", SortOrder.Descending); + Assert.Same(item2, collection.Add(item2)); + Assert.Same(item2, collection[1]); + + IList list = CreateCollection(); + int index = list.Add(item1); + Assert.Equal(0, index); + Assert.Same(item1, list[0]); + index = list.Add(item2); + Assert.Equal(1, index); + Assert.Same(item2, list[1]); + } + + [Fact] + public void Add_HelperOverloads_ItemsAddedAsExpected() + { + SqlBulkCopyColumnOrderHintCollection collection = CreateCollection(); + SqlBulkCopyColumnOrderHint item; + + item = collection.Add("column1", SortOrder.Ascending); + Assert.NotNull(item); + Assert.Equal("column1", item.Column); + Assert.Equal(SortOrder.Ascending, item.SortOrder); + } + + [Fact] + public void Add_InvalidItems_ThrowsArgumentException() + { + SqlBulkCopyColumnOrderHintCollection collection = CreateCollection(); + Assert.Throws(() => collection.Add(new SqlBulkCopyColumnOrderHint(null, SortOrder.Ascending))); + Assert.Throws(() => collection.Add(new SqlBulkCopyColumnOrderHint(null, SortOrder.Unspecified))); + Assert.Throws(() => collection.Add(new SqlBulkCopyColumnOrderHint("", SortOrder.Descending))); + Assert.Throws(() => collection.Add(new SqlBulkCopyColumnOrderHint("", SortOrder.Unspecified))); + Assert.Throws(() => collection.Add(new SqlBulkCopyColumnOrderHint("column", SortOrder.Unspecified))); + + IList list = CreateCollection(); + Assert.Throws(() => list.Add(new SqlBulkCopyColumnOrderHint(null, SortOrder.Ascending))); + Assert.Throws(() => list.Add(new SqlBulkCopyColumnOrderHint(null, SortOrder.Unspecified))); + Assert.Throws(() => list.Add(new SqlBulkCopyColumnOrderHint("", SortOrder.Descending))); + Assert.Throws(() => list.Add(new SqlBulkCopyColumnOrderHint("", SortOrder.Unspecified))); + Assert.Throws(() => list.Add(new SqlBulkCopyColumnOrderHint("column", SortOrder.Unspecified))); + } + + [Fact] + public void IListAddInsert_InsertNonSqlBulkCopyColumnOrderHintItems_DoNotThrow() + { + IList list = CreateCollection(); + list.Add(new SqlBulkCopyColumnOrderHint("column", SortOrder.Descending)); + + // The following operations should really throw ArgumentException due to the + // mismatched types, but do not throw in the full framework. + string foo = "foo"; + list[0] = foo; + list.Add(foo); + list.Insert(0, foo); + } + + [Fact] + public void GetEnumerator_NoItems_EmptyEnumerator() + { + SqlBulkCopyColumnOrderHintCollection collection = CreateCollection(); + IEnumerator e = collection.GetEnumerator(); + Assert.Throws(() => e.Current); + Assert.False(e.MoveNext()); + Assert.Throws(() => e.Current); + } + + [Fact] + public void GetEnumerator_ItemsAdded_AllItemsReturnedAndEnumeratorBehavesAsExpected() + { + var item1 = new SqlBulkCopyColumnOrderHint("column1", SortOrder.Ascending); + var item2 = new SqlBulkCopyColumnOrderHint("column2", SortOrder.Descending); + var item3 = new SqlBulkCopyColumnOrderHint("column3", SortOrder.Descending); + + SqlBulkCopyColumnOrderHintCollection collection = CreateCollection(item1, item2, item3); + + IEnumerator e = collection.GetEnumerator(); + + const int Iterations = 2; + for (int i = 0; i < Iterations; i++) + { + // Not started + Assert.Throws(() => e.Current); + + Assert.True(e.MoveNext()); + Assert.Same(item1, e.Current); + + Assert.True(e.MoveNext()); + Assert.Same(item2, e.Current); + + Assert.True(e.MoveNext()); + Assert.Same(item3, e.Current); + + Assert.False(e.MoveNext()); + Assert.False(e.MoveNext()); + Assert.False(e.MoveNext()); + Assert.False(e.MoveNext()); + Assert.False(e.MoveNext()); + + // Ended + Assert.Throws(() => e.Current); + + e.Reset(); + } + } + + [Fact] + public void GetEnumerator_ItemsAdded_ItemsFromEnumeratorMatchesItemsFromIndexer() + { + var item1 = new SqlBulkCopyColumnOrderHint("column1", SortOrder.Ascending); + var item2 = new SqlBulkCopyColumnOrderHint("column2", SortOrder.Descending); + var item3 = new SqlBulkCopyColumnOrderHint("column3", SortOrder.Descending); + + SqlBulkCopyColumnOrderHintCollection collection = CreateCollection(item1, item2, item3); + int index = 0; + foreach (SqlBulkCopyColumnOrderHint enumeratorItem in collection) + { + SqlBulkCopyColumnOrderHint indexerItem = collection[index]; + + Assert.NotNull(enumeratorItem); + Assert.NotNull(indexerItem); + + Assert.Same(indexerItem, enumeratorItem); + index++; + } + } + + [Fact] + public void GetEnumerator_ModifiedCollectionDuringEnumeration_ThrowsInvalidOperationException() + { + SqlBulkCopyColumnOrderHintCollection collection = CreateCollection(); + + IEnumerator e = collection.GetEnumerator(); + + collection.Add("column", SortOrder.Ascending); + + // Collection changed. + Assert.Throws(() => e.MoveNext()); + Assert.Throws(() => e.Reset()); + } + + [Fact] + public void Contains_ItemsAdded_MatchesExpectation() + { + var item1 = new SqlBulkCopyColumnOrderHint("column1", SortOrder.Ascending); + var item2 = new SqlBulkCopyColumnOrderHint("column2", SortOrder.Descending); + var item3 = new SqlBulkCopyColumnOrderHint("column3", SortOrder.Ascending); + + SqlBulkCopyColumnOrderHintCollection collection = CreateCollection(item1, item2, item3); + + Assert.True(collection.Contains(item1)); + Assert.True(collection.Contains(item2)); + Assert.True(collection.Contains(item3)); + Assert.False(collection.Contains(null)); + + IList list = collection; + Assert.True(list.Contains(item1)); + Assert.True(list.Contains(item2)); + Assert.True(list.Contains(item3)); + Assert.False(list.Contains(null)); + Assert.False(list.Contains("Bogus")); + } + + [Fact] + public void CopyTo_ItemsAdded_ItemsCopiedToArray() + { + var item1 = new SqlBulkCopyColumnOrderHint("column1", SortOrder.Ascending); + var item2 = new SqlBulkCopyColumnOrderHint("column2", SortOrder.Descending); + var item3 = new SqlBulkCopyColumnOrderHint("column3", SortOrder.Ascending); + + SqlBulkCopyColumnOrderHintCollection collection = CreateCollection(item1, item2, item3); + + var array1 = new SqlBulkCopyColumnOrderHint[collection.Count]; + collection.CopyTo(array1, 0); + + Assert.Same(item1, array1[0]); + Assert.Same(item2, array1[1]); + Assert.Same(item3, array1[2]); + + var array2 = new SqlBulkCopyColumnOrderHint[collection.Count]; + ((ICollection)collection).CopyTo(array2, 0); + + Assert.Same(item1, array2[0]); + Assert.Same(item2, array2[1]); + Assert.Same(item3, array2[2]); + } + + [Fact] + public void CopyTo_InvalidArrayType_Throws() + { + var item1 = new SqlBulkCopyColumnOrderHint("column1", SortOrder.Ascending); + var item2 = new SqlBulkCopyColumnOrderHint("column2", SortOrder.Descending); + var item3 = new SqlBulkCopyColumnOrderHint("column3", SortOrder.Ascending); + + ICollection collection = CreateCollection(item1, item2, item3); + + Assert.Throws(() => collection.CopyTo(new int[collection.Count], 0)); + Assert.Throws(() => collection.CopyTo(new string[collection.Count], 0)); + } + + [Fact] + public void Indexer_BehavesAsExpected() + { + var item1 = new SqlBulkCopyColumnOrderHint("column1", SortOrder.Ascending); + var item2 = new SqlBulkCopyColumnOrderHint("column2", SortOrder.Descending); + var item3 = new SqlBulkCopyColumnOrderHint("column3", SortOrder.Ascending); + + SqlBulkCopyColumnOrderHintCollection collection = CreateCollection(item1, item2, item3); + + Assert.Same(item1, collection[0]); + Assert.Same(item2, collection[1]); + Assert.Same(item3, collection[2]); + + IList list = collection; + list[0] = item2; + list[1] = item3; + list[2] = item1; + Assert.Same(item2, list[0]); + Assert.Same(item3, list[1]); + Assert.Same(item1, list[2]); + } + + [Fact] + public void IndexOf_BehavesAsExpected() + { + var item1 = new SqlBulkCopyColumnOrderHint("column1", SortOrder.Ascending); + var item2 = new SqlBulkCopyColumnOrderHint("column2", SortOrder.Descending); + var item3 = new SqlBulkCopyColumnOrderHint("column3", SortOrder.Ascending); + + SqlBulkCopyColumnOrderHintCollection collection = CreateCollection(item1, item2); + + Assert.Equal(0, collection.IndexOf(item1)); + Assert.Equal(1, collection.IndexOf(item2)); + Assert.Equal(-1, collection.IndexOf(item3)); + + IList list = collection; + Assert.Equal(0, list.IndexOf(item1)); + Assert.Equal(1, list.IndexOf(item2)); + Assert.Equal(-1, list.IndexOf(item3)); + Assert.Equal(-1, list.IndexOf("bogus")); + } + + [Fact] + public void Insert_BehavesAsExpected() + { + var item1 = new SqlBulkCopyColumnOrderHint("column1", SortOrder.Ascending); + var item2 = new SqlBulkCopyColumnOrderHint("column2", SortOrder.Descending); + var item3 = new SqlBulkCopyColumnOrderHint("column3", SortOrder.Ascending); + + SqlBulkCopyColumnOrderHintCollection collection = CreateCollection(); + + collection.Insert(0, item3); + collection.Insert(0, item2); + collection.Insert(0, item1); + + Assert.Equal(3, collection.Count); + Assert.Same(item1, collection[0]); + Assert.Same(item2, collection[1]); + Assert.Same(item3, collection[2]); + } + + [Fact] + public void InsertAndClear_BehavesAsExpected() + { + var item1 = new SqlBulkCopyColumnOrderHint("column1", SortOrder.Ascending); + var item2 = new SqlBulkCopyColumnOrderHint("column2", SortOrder.Descending); + var item3 = new SqlBulkCopyColumnOrderHint("column3", SortOrder.Ascending); + + SqlBulkCopyColumnOrderHintCollection collection = CreateCollection(); + + collection.Insert(0, item1); + collection.Insert(1, item2); + collection.Insert(2, item3); + Assert.Equal(3, collection.Count); + Assert.Same(item1, collection[0]); + Assert.Same(item2, collection[1]); + Assert.Same(item3, collection[2]); + + collection.Clear(); + Assert.Empty(collection); + + collection.Add(item1); + collection.Add(item3); + Assert.Equal(2, collection.Count); + Assert.Same(item1, collection[0]); + Assert.Same(item3, collection[1]); + + collection.Insert(1, item2); + Assert.Equal(3, collection.Count); + Assert.Same(item1, collection[0]); + Assert.Same(item2, collection[1]); + Assert.Same(item3, collection[2]); + + collection.Clear(); + Assert.Empty(collection); + + IList list = collection; + list.Insert(0, item1); + list.Insert(1, item2); + list.Insert(2, item3); + Assert.Equal(3, list.Count); + Assert.Same(item1, list[0]); + Assert.Same(item2, list[1]); + Assert.Same(item3, list[2]); + + list.Clear(); + Assert.Equal(0, list.Count); + + list.Add(item1); + list.Add(item3); + Assert.Equal(2, list.Count); + Assert.Same(item1, list[0]); + Assert.Same(item3, list[1]); + + list.Insert(1, item2); + Assert.Equal(3, list.Count); + Assert.Same(item1, list[0]); + Assert.Same(item2, list[1]); + Assert.Same(item3, list[2]); + + list.Clear(); + Assert.Equal(0, list.Count); + } + + [Fact] + public void Remove_BehavesAsExpected() + { + var item1 = new SqlBulkCopyColumnOrderHint("column1", SortOrder.Ascending); + var item2 = new SqlBulkCopyColumnOrderHint("column2", SortOrder.Descending); + + SqlBulkCopyColumnOrderHintCollection collection = CreateCollection(item1, item2); + + collection.Remove(item1); + Assert.Single(collection); + Assert.Same(item2, collection[0]); + + collection.Remove(item2); + Assert.Empty(collection); + + AssertExtensions.Throws(() => collection.Remove(item2)); + AssertExtensions.Throws(() => collection.Remove(new SqlBulkCopyColumnOrderHint("column3", SortOrder.Descending))); + + IList list = CreateCollection(item1, item2); + + list.Remove(item1); + Assert.Equal(1, list.Count); + Assert.Same(item2, list[0]); + + list.Remove(item2); + Assert.Equal(0, list.Count); + + AssertExtensions.Throws(null, () => list.Remove(item2)); + AssertExtensions.Throws(null, () => list.Remove(new SqlBulkCopyColumnOrderHint("column4", SortOrder.Ascending))); + AssertExtensions.Throws(null, () => list.Remove("bogus")); + } + + [Fact] + public void RemoveAt_BehavesAsExpected() + { + var item1 = new SqlBulkCopyColumnOrderHint("column1", SortOrder.Ascending); + var item2 = new SqlBulkCopyColumnOrderHint("column2", SortOrder.Descending); + var item3 = new SqlBulkCopyColumnOrderHint("column3", SortOrder.Ascending); + + SqlBulkCopyColumnOrderHintCollection collection = CreateCollection(item1, item2, item3); + + collection.RemoveAt(0); + Assert.Equal(2, collection.Count); + Assert.Same(item2, collection[0]); + Assert.Same(item3, collection[1]); + + collection.RemoveAt(1); + Assert.Single(collection); + Assert.Same(item2, collection[0]); + + collection.RemoveAt(0); + Assert.Empty(collection); + + IList list = CreateCollection(item1, item2, item3); + + list.RemoveAt(0); + Assert.Equal(2, list.Count); + Assert.Same(item2, list[0]); + Assert.Same(item3, list[1]); + list.RemoveAt(1); + Assert.Equal(1, list.Count); + Assert.Same(item2, list[0]); + + list.RemoveAt(0); + Assert.Equal(0, list.Count); + } + + [Fact] + public void SyncRoot_NotNullAndSameObject() + { + ICollection collection = CreateCollection(); + Assert.NotNull(collection.SyncRoot); + Assert.Same(collection.SyncRoot, collection.SyncRoot); + } + + [Fact] + public void Add_DuplicateColumnNames_NotAllowed() + { + SqlBulkCopyColumnOrderHintCollection collection1 = CreateCollection(); + SqlBulkCopyColumnOrderHintCollection collection2 = CreateCollection(); + var item1 = new SqlBulkCopyColumnOrderHint("column1", SortOrder.Ascending); + + collection1.Add(item1); + item1.Column += "2"; + collection1[0].Column += "3"; + Assert.Equal("column123", item1.Column); + Assert.Throws(() => collection1.Add(item1)); + + collection2.Add(item1); + item1.Column += "4"; + Assert.Same(collection1[0], collection2[0]); + Assert.Equal("column1234", collection1[0].Column); + + item1.Column = "column1"; + collection1.Add("column2", SortOrder.Ascending); + item1.Column = "column1"; + collection1[0].Column = "column1"; + collection1[1].Column = "column2"; + Assert.Throws(() => item1.Column = "column2"); + Assert.Throws(() => collection1[0].Column = "column2"); + Assert.Throws(() => collection1[1].Column = "column1"); + Assert.Throws(() => collection2[0].Column = "column2"); + Assert.Equal("column1", collection1[0].Column); + Assert.Equal("column2", collection1[1].Column); + ValidateCollection(collection1, expectedCount: 2); + + Assert.Throws(() => collection2.Add(collection1[0])); + collection2.Add(collection1[1]); + var item3 = new SqlBulkCopyColumnOrderHint("column1", SortOrder.Ascending); + Assert.Throws(() => collection1.Add(item3)); + item3.Column = "column3"; + collection1.Add(item3); + ValidateCollection(collection1, expectedCount: 3); + ValidateCollection(collection2, expectedCount: 2); + + Assert.Throws(() => collection1.Insert(0, new SqlBulkCopyColumnOrderHint("column3", SortOrder.Ascending))); + collection1.Insert(0, new SqlBulkCopyColumnOrderHint("column4", SortOrder.Ascending)); + collection1.Insert(collection1.Count, new SqlBulkCopyColumnOrderHint("column5", SortOrder.Ascending)); + Assert.Throws(() => collection1[collection1.IndexOf(item1)].Column = "column4"); + ValidateCollection(collection1, expectedCount: 5); + + collection2.Remove(item1); + Assert.Throws(() => collection2[0].Column = item1.Column); + collection2[0].Column = "column6"; + ValidateCollection(collection2, expectedCount: 1); + + collection1.Clear(); + Assert.Empty(collection1); + collection1.Add("column1", SortOrder.Descending); + collection1.Add("column2", SortOrder.Descending); + collection1.Add("column3", SortOrder.Descending); + collection2[0].Column = collection1[0].Column; + Assert.Throws(() => collection1.Add(collection2[0])); + ValidateCollection(collection1, expectedCount: 3); + collection1.RemoveAt(0); + collection1.Add(collection2[0]); + collection1.Remove(collection1[collection1.Count - 1]); + collection1.RemoveAt(1); + collection1.Remove(collection1[collection1.Count - 1]); + collection1.Add("column1", SortOrder.Descending); + collection1.Add("column2", SortOrder.Descending); + collection1.Add("column3", SortOrder.Descending); + collection1.Add("column4", SortOrder.Descending); + collection1.Add("column5", SortOrder.Descending); + ValidateCollection(collection1, expectedCount: 5); + } + + // verifies that the collection contains no duplicate column names + private void ValidateCollection(SqlBulkCopyColumnOrderHintCollection collection, int expectedCount) + { + Assert.True(expectedCount == collection.Count, "Collection was not the expected size."); + bool valid = true; + HashSet columnNames = new HashSet(); + foreach (SqlBulkCopyColumnOrderHint orderHint in collection) + { + if (!columnNames.Contains(orderHint.Column)) + { + columnNames.Add(orderHint.Column); + } + else + { + valid = false; + } + } + Assert.True(valid, "Collection contained a duplicate column name."); + } + } +} 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 52c66b3d1b..d0152fca05 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 @@ -61,6 +61,12 @@ Common\System\Collections\DictionaryExtensions.cs + + + + + + diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHint.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHint.cs new file mode 100644 index 0000000000..7145679431 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHint.cs @@ -0,0 +1,150 @@ +// 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 Xunit; + +namespace Microsoft.Data.SqlClient.ManualTesting.Tests +{ + public class OrderHint + { + private static readonly string sourceTable = "Customers"; + private static readonly string sourceTable2 = "Employees"; + private static readonly string initialQueryTemplate = "create table {0} (CustomerID nvarchar(50), CompanyName nvarchar(50), ContactName nvarchar(50)); CREATE CLUSTERED INDEX IX_Test_Table_Customer_ID ON {0} (CustomerID DESC)"; + private static readonly string initialQueryTemplate2 = "create table {0} (LastName nvarchar(50), FirstName nvarchar(50))"; + private static readonly string sourceQueryTemplate = "SELECT CustomerID, CompanyName, ContactName FROM {0}"; + private static readonly string sourceQueryTemplate2 = "SELECT LastName, FirstName FROM {0}"; + private static readonly string getRowCountQueryTemplate = "SELECT COUNT(*) FROM {0}"; + + public static void Test(string srcConstr, string dstTable, string dstTable2) + { + srcConstr = (new SqlConnectionStringBuilder(srcConstr) { InitialCatalog = "Northwind" }).ConnectionString; + string dstConstr = (new SqlConnectionStringBuilder(srcConstr)).ConnectionString; + string sourceQuery = string.Format(sourceQueryTemplate, sourceTable); + string sourceQuery2 = string.Format(sourceQueryTemplate2, sourceTable2); + string initialQuery = string.Format(initialQueryTemplate, dstTable); + string initialQuery2 = string.Format(initialQueryTemplate2, dstTable2); + string getRowCountQuery = string.Format(getRowCountQueryTemplate, sourceTable); + string getRowCountQuery2 = string.Format(getRowCountQueryTemplate, sourceTable2); + + using (SqlConnection dstConn = new SqlConnection(dstConstr)) + using (SqlCommand dstCmd = dstConn.CreateCommand()) + { + dstConn.Open(); + Helpers.TryExecute(dstCmd, initialQuery); + Helpers.TryExecute(dstCmd, initialQuery2); + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand(getRowCountQuery, srcConn)) + { + srcConn.Open(); + try + { + int nRowsInSource = Convert.ToInt32(srcCmd.ExecuteScalar()); + srcCmd.CommandText = getRowCountQuery2; + int nRowsInSource2 = Convert.ToInt32(srcCmd.ExecuteScalar()); + srcCmd.CommandText = sourceQuery; + using (SqlBulkCopy bulkCopy = new SqlBulkCopy(dstConn)) + { + bulkCopy.DestinationTableName = dstTable; + + // no hints + using (DbDataReader reader = srcCmd.ExecuteReader()) + { + bulkCopy.WriteToServer(reader); + } + + // hint for 1 of 3 columns + using (DbDataReader reader = srcCmd.ExecuteReader()) + { + bulkCopy.ColumnOrderHints.Add("CustomerID", SortOrder.Ascending); + bulkCopy.WriteToServer(reader); + } + + // hints for all 3 columns + // order of hints is not the same as column order in table + using (DbDataReader reader = srcCmd.ExecuteReader()) + { + bulkCopy.ColumnOrderHints.Add("ContactName", SortOrder.Descending); + bulkCopy.ColumnOrderHints.Add("CompanyName", SortOrder.Ascending); + bulkCopy.WriteToServer(reader); + } + + // add column mappings + using (DbDataReader reader = srcCmd.ExecuteReader()) + { + bulkCopy.ColumnMappings.Add(0, 1); + bulkCopy.ColumnMappings.Add(1, 2); + bulkCopy.ColumnMappings.Add(2, 0); + bulkCopy.WriteToServer(reader); + } + + // WriteToServer DataTable overload + using (SqlDataAdapter dataAdapter = new SqlDataAdapter(srcCmd)) + { + DataTable dataTable = new DataTable(); + dataAdapter.Fill(dataTable); + bulkCopy.WriteToServer(dataTable); + } + + // WriteToServer DataRow[] overload + using (SqlDataAdapter dataAdapter = new SqlDataAdapter(srcCmd)) + { + DataTable dataTable = new DataTable(); + dataAdapter.Fill(dataTable); + bulkCopy.WriteToServer(dataTable.Select()); + } + + // WriteToServer DataTable, DataRowState overload + using (SqlDataAdapter dataAdapter = new SqlDataAdapter(srcCmd)) + { + DataTable dataTable = new DataTable(); + dataAdapter.Fill(dataTable); + DataRow[] x = dataTable.Select(); + bulkCopy.WriteToServer(dataTable, DataRowState.Unchanged); + } + } + + // add copy options + SqlBulkCopyOptions copyOptions = + SqlBulkCopyOptions.AllowEncryptedValueModifications | + SqlBulkCopyOptions.FireTriggers | + SqlBulkCopyOptions.KeepNulls; + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn, copyOptions, null)) + { + bulkcopy.DestinationTableName = dstTable; + bulkcopy.ColumnOrderHints.Add("CustomerID", SortOrder.Ascending); + bulkcopy.ColumnOrderHints.Add("CompanyName", SortOrder.Ascending); + bulkcopy.ColumnOrderHints.Add("ContactName", SortOrder.Descending); + using (DbDataReader reader = srcCmd.ExecuteReader()) + { + bulkcopy.WriteToServer(reader); + } + + const int nWriteToServerCalls = 8; + Helpers.VerifyResults(dstConn, dstTable, 3, nRowsInSource * nWriteToServerCalls); + + // different tables + srcCmd.CommandText = sourceQuery2; + using (DbDataReader reader = srcCmd.ExecuteReader()) + { + bulkcopy.DestinationTableName = dstTable2; + bulkcopy.ColumnOrderHints.Clear(); + bulkcopy.ColumnOrderHints.Add("LastName", SortOrder.Descending); + bulkcopy.WriteToServer(reader); + Helpers.VerifyResults(dstConn, dstTable2, 2, nRowsInSource2); + } + } + } + finally + { + DataTestUtility.DropTable(dstConn, dstTable); + DataTestUtility.DropTable(dstConn, dstTable2); + } + } + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintAsync.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintAsync.cs new file mode 100644 index 0000000000..d9c4515428 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintAsync.cs @@ -0,0 +1,132 @@ +// 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 class OrderHintAsync + { + private static readonly string sourceTable = "Customers"; + private static readonly string sourceTable2 = "Employees"; + private static readonly string initialQueryTemplate = "create table {0} (CustomerID nvarchar(50), CompanyName nvarchar(50), ContactName nvarchar(50)); CREATE CLUSTERED INDEX IX_Test_Table_Customer_ID ON {0} (CustomerID DESC)"; + private static readonly string initialQueryTemplate2 = "create table {0} (LastName nvarchar(50), FirstName nvarchar(50))"; + private static readonly string sourceQueryTemplate = "SELECT CustomerID, CompanyName, ContactName FROM {0}"; + private static readonly string sourceQueryTemplate2 = "SELECT LastName, FirstName FROM {0}"; + private static readonly string getRowCountQueryTemplate = "SELECT COUNT(*) FROM {0}"; + + public static void Test(string srcConstr, string dstTable, string dstTable2) + { + Task t = TestAsync(srcConstr, dstTable, dstTable2); + t.Wait(); + Assert.True(t.IsCompleted, "Task did not complete! Status: " + t.Status); + } + + public static async Task TestAsync(string srcConstr, string dstTable, string dstTable2) + { + srcConstr = (new SqlConnectionStringBuilder(srcConstr) { InitialCatalog = "Northwind" }).ConnectionString; + string dstConstr = (new SqlConnectionStringBuilder(srcConstr)).ConnectionString; + string sourceQuery = string.Format(sourceQueryTemplate, sourceTable); + string sourceQuery2 = string.Format(sourceQueryTemplate2, sourceTable2); + string initialQuery = string.Format(initialQueryTemplate, dstTable); + string initialQuery2 = string.Format(initialQueryTemplate2, dstTable2); + string getRowCountQuery = string.Format(getRowCountQueryTemplate, sourceTable); + string getRowCountQuery2 = string.Format(getRowCountQueryTemplate, sourceTable2); + + using (SqlConnection dstConn = new SqlConnection(dstConstr)) + using (SqlCommand dstCmd = dstConn.CreateCommand()) + { + await dstConn.OpenAsync(); + Helpers.TryExecute(dstCmd, initialQuery); + Helpers.TryExecute(dstCmd, initialQuery2); + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand(getRowCountQuery, srcConn)) + { + await srcConn.OpenAsync(); + try + { + int nRowsInSource = Convert.ToInt32(await srcCmd.ExecuteScalarAsync()); + srcCmd.CommandText = getRowCountQuery2; + int nRowsInSource2 = Convert.ToInt32(await srcCmd.ExecuteScalarAsync()); + srcCmd.CommandText = sourceQuery; + using (SqlBulkCopy bulkCopy = new SqlBulkCopy(dstConn)) + { + bulkCopy.DestinationTableName = dstTable; + + // no hints + using (DbDataReader reader = await srcCmd.ExecuteReaderAsync()) + { + await bulkCopy.WriteToServerAsync(reader); + } + + // hint for 1 of 3 columns + using (DbDataReader reader = await srcCmd.ExecuteReaderAsync()) + { + bulkCopy.ColumnOrderHints.Add("CustomerID", SortOrder.Ascending); + await bulkCopy.WriteToServerAsync(reader); + } + + // hints for all 3 columns + // order of hints is not the same as column order in table + using (DbDataReader reader = await srcCmd.ExecuteReaderAsync()) + { + bulkCopy.ColumnOrderHints.Add("ContactName", SortOrder.Descending); + bulkCopy.ColumnOrderHints.Add("CompanyName", SortOrder.Ascending); + await bulkCopy.WriteToServerAsync(reader); + } + + // add column mappings + using (DbDataReader reader = await srcCmd.ExecuteReaderAsync()) + { + bulkCopy.ColumnMappings.Add(0, 1); + bulkCopy.ColumnMappings.Add(1, 2); + bulkCopy.ColumnMappings.Add(2, 0); + await bulkCopy.WriteToServerAsync(reader); + } + } + + // add copy options + SqlBulkCopyOptions copyOptions = SqlBulkCopyOptions.AllowEncryptedValueModifications + | SqlBulkCopyOptions.FireTriggers + | SqlBulkCopyOptions.KeepNulls; + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn, copyOptions, null)) + { + bulkcopy.DestinationTableName = dstTable; + bulkcopy.ColumnOrderHints.Add("CustomerID", SortOrder.Ascending); + bulkcopy.ColumnOrderHints.Add("CompanyName", SortOrder.Ascending); + bulkcopy.ColumnOrderHints.Add("ContactName", SortOrder.Descending); + using (DbDataReader reader = await srcCmd.ExecuteReaderAsync()) + { + await bulkcopy.WriteToServerAsync(reader); + } + + const int nWriteToServerCalls = 5; + Helpers.VerifyResults(dstConn, dstTable, 3, nRowsInSource * nWriteToServerCalls); + + // different tables + srcCmd.CommandText = sourceQuery2; + using (DbDataReader reader = await srcCmd.ExecuteReaderAsync()) + { + bulkcopy.DestinationTableName = dstTable2; + bulkcopy.ColumnOrderHints.Clear(); + bulkcopy.ColumnOrderHints.Add("LastName", SortOrder.Descending); + await bulkcopy.WriteToServerAsync(reader); + Helpers.VerifyResults(dstConn, dstTable2, 2, nRowsInSource2); + } + } + } + finally + { + DataTestUtility.DropTable(dstConn, dstTable); + DataTestUtility.DropTable(dstConn, dstTable2); + } + } + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintDuplicateColumn.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintDuplicateColumn.cs new file mode 100644 index 0000000000..59e82a8a97 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintDuplicateColumn.cs @@ -0,0 +1,70 @@ +// 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.Common; +using Xunit; + +namespace Microsoft.Data.SqlClient.ManualTesting.Tests +{ + public class OrderHintDuplicateColumn + { + private static readonly string sourceTable = "Customers"; + private static readonly string initialQueryTemplate = "create table {0} (CustomerID nvarchar(50), CompanyName nvarchar(50), ContactName nvarchar(50))"; + private static readonly string sourceQueryTemplate = "SELECT CustomerID, CompanyName, ContactName FROM {0}"; + private static readonly string getRowCountQueryTemplate = "SELECT COUNT(*) FROM {0}"; + + public static void Test(string srcConstr, string dstTable) + { + srcConstr = (new SqlConnectionStringBuilder(srcConstr) { InitialCatalog = "Northwind" }).ConnectionString; + string dstConstr = (new SqlConnectionStringBuilder(srcConstr)).ConnectionString; + string initialQuery = string.Format(initialQueryTemplate, dstTable); + string sourceQuery = string.Format(sourceQueryTemplate, sourceTable); + string getRowCountQuery = string.Format(getRowCountQueryTemplate, sourceTable); + + using (SqlConnection dstConn = new SqlConnection(dstConstr)) + using (SqlCommand dstCmd = dstConn.CreateCommand()) + { + dstConn.Open(); + Helpers.TryExecute(dstCmd, initialQuery); + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand(getRowCountQuery, srcConn)) + { + srcConn.Open(); + try + { + int nRowsInSource = Convert.ToInt32(srcCmd.ExecuteScalar()); + srcCmd.CommandText = sourceQuery; + using (DbDataReader reader = srcCmd.ExecuteReader()) + { + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) + { + bulkcopy.DestinationTableName = dstTable; + const string destColumn = "CompanyName"; + const string destColumn2 = "ContactName"; + bulkcopy.ColumnOrderHints.Add(destColumn, SortOrder.Ascending); + + string expectedErrorMsg = string.Format( + SystemDataResourceManager.Instance.SQL_BulkLoadOrderHintDuplicateColumn, destColumn); + DataTestUtility.AssertThrowsWrapper( + () => bulkcopy.ColumnOrderHints.Add(destColumn, SortOrder.Ascending), + exceptionMessage: expectedErrorMsg); + + bulkcopy.ColumnOrderHints.Add(destColumn2, SortOrder.Ascending); + Assert.Equal(2, bulkcopy.ColumnOrderHints.Count); + + bulkcopy.WriteToServer(reader); + Helpers.VerifyResults(dstConn, dstTable, 3, nRowsInSource); + } + } + } + finally + { + DataTestUtility.DropTable(dstConn, dstTable); + } + } + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintIdentityColumn.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintIdentityColumn.cs new file mode 100644 index 0000000000..a95c13ad36 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintIdentityColumn.cs @@ -0,0 +1,67 @@ +// 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.Common; + +namespace Microsoft.Data.SqlClient.ManualTesting.Tests +{ + public class OrderHintIdentityColumn + { + private static readonly string sourceTable = "Customers"; + private static readonly string initialQueryTemplate = "create table {0} (CustomerID int IDENTITY NOT NULL, CompanyName nvarchar(50), ContactName nvarchar(50))"; + private static readonly string sourceQueryTemplate = "SELECT CustomerId, CompanyName, ContactName FROM {0}"; + private static readonly string getRowCountQueryTemplate = "SELECT COUNT(*) FROM {0}"; + + public static void Test(string srcConstr, string dstTable) + { + srcConstr = (new SqlConnectionStringBuilder(srcConstr) { InitialCatalog = "Northwind" }).ConnectionString; + string dstConstr = (new SqlConnectionStringBuilder(srcConstr)).ConnectionString; + string sourceQuery = string.Format(sourceQueryTemplate, sourceTable); + string initialQuery = string.Format(initialQueryTemplate, dstTable); + string getRowCountQuery = string.Format(getRowCountQueryTemplate, sourceTable); + + using (SqlConnection dstConn = new SqlConnection(dstConstr)) + using (SqlCommand dstCmd = dstConn.CreateCommand()) + { + dstConn.Open(); + Helpers.TryExecute(dstCmd, initialQuery); + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand(getRowCountQuery, srcConn)) + { + srcConn.Open(); + try + { + int nRowsInSource = Convert.ToInt32(srcCmd.ExecuteScalar()); + srcCmd.CommandText = sourceQuery; + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) + { + bulkcopy.DestinationTableName = dstTable; + + // no mapping + using (DbDataReader reader = srcCmd.ExecuteReader()) + { + bulkcopy.ColumnOrderHints.Add("CustomerID", SortOrder.Ascending); + bulkcopy.WriteToServer(reader); + Helpers.VerifyResults(dstConn, dstTable, 3, nRowsInSource); + } + + // with mapping + using (DbDataReader reader = srcCmd.ExecuteReader()) + { + bulkcopy.ColumnMappings.Add(0, 1); + bulkcopy.WriteToServer(reader); + Helpers.VerifyResults(dstConn, dstTable, 3, nRowsInSource); + } + } + } + finally + { + DataTestUtility.DropTable(dstConn, dstTable); + } + } + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintMissingTargetColumn.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintMissingTargetColumn.cs new file mode 100644 index 0000000000..dcfeb6e311 --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintMissingTargetColumn.cs @@ -0,0 +1,75 @@ +// 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.Common; +using Xunit; + +namespace Microsoft.Data.SqlClient.ManualTesting.Tests +{ + public class OrderHintMissingTargetColumn + { + private static readonly string sourceTable = "Customers"; + private static readonly string initialQueryTemplate = "create table {0} (CustomerID nvarchar(50), CompanyName nvarchar(50), ContactName nvarchar(50))"; + private static readonly string sourceQueryTemplate = "SELECT CustomerID, CompanyName, ContactName FROM {0}"; + + public static void Test(string srcConstr, string dstTable) + { + srcConstr = (new SqlConnectionStringBuilder(srcConstr) { InitialCatalog = "Northwind" }).ConnectionString; + string dstConstr = (new SqlConnectionStringBuilder(srcConstr)).ConnectionString; + string initialQuery = string.Format(initialQueryTemplate, dstTable); + string sourceQuery = string.Format(sourceQueryTemplate, sourceTable); + + using (SqlConnection dstConn = new SqlConnection(dstConstr)) + using (SqlCommand dstCmd = dstConn.CreateCommand()) + { + dstConn.Open(); + Helpers.TryExecute(dstCmd, initialQuery); + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand(sourceQuery, srcConn)) + { + srcConn.Open(); + using (DbDataReader reader = srcCmd.ExecuteReader()) + { + using (SqlBulkCopy bulkcopy = new SqlBulkCopy(dstConn)) + { + try + { + bulkcopy.DestinationTableName = dstTable; + const string nonexistentColumn = "nonexistent column"; + const string sourceColumn = "CustomerID"; + const string destColumn = "ContactName"; + + // column does not exist in destination table + bulkcopy.ColumnOrderHints.Add(nonexistentColumn, SortOrder.Ascending); + + string expectedErrorMsg = string.Format( + SystemDataResourceManager.Instance.SQL_BulkLoadOrderHintInvalidColumn, nonexistentColumn); + DataTestUtility.AssertThrowsWrapper( + () => bulkcopy.WriteToServer(reader), + exceptionMessage: expectedErrorMsg); + + // column does not exist in destination table because of user-defined mapping + bulkcopy.ColumnMappings.Add(sourceColumn, destColumn); + bulkcopy.ColumnOrderHints.RemoveAt(0); + bulkcopy.ColumnOrderHints.Add(sourceColumn, SortOrder.Ascending); + Assert.True(bulkcopy.ColumnOrderHints.Count == 1, "Error adding a column order hint"); + + expectedErrorMsg = string.Format( + SystemDataResourceManager.Instance.SQL_BulkLoadOrderHintInvalidColumn, sourceColumn); + DataTestUtility.AssertThrowsWrapper( + () => bulkcopy.WriteToServer(reader), + exceptionMessage: expectedErrorMsg); + } + finally + { + DataTestUtility.DropTable(dstConn, dstTable); + } + } + } + } + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintTransaction.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintTransaction.cs new file mode 100644 index 0000000000..73dae137aa --- /dev/null +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/OrderHintTransaction.cs @@ -0,0 +1,58 @@ +// 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.ManualTesting.Tests +{ + class OrderHintTransaction + { + private static readonly string sourceTable = "Customers"; + private static readonly string initialQueryTemplate = "create table {0} (CustomerID nvarchar(50), CompanyName nvarchar(50), ContactName nvarchar(50))"; + private static readonly string sourceQueryTemplate = "SELECT CustomerID, CompanyName, ContactName FROM {0}"; + + public static void Test(string srcConstr, string dstTable) + { + srcConstr = (new SqlConnectionStringBuilder(srcConstr) { InitialCatalog = "Northwind" }).ConnectionString; + string dstConstr = (new SqlConnectionStringBuilder(srcConstr)).ConnectionString; + string initialQuery = string.Format(initialQueryTemplate, dstTable); + string sourceQuery = string.Format(sourceQueryTemplate, sourceTable); + + using (SqlConnection dstConn = new SqlConnection(dstConstr)) + using (SqlCommand dstCmd = dstConn.CreateCommand()) + { + dstConn.Open(); + SqlTransaction txn = dstConn.BeginTransaction(); + dstCmd.Transaction = txn; + Helpers.TryExecute(dstCmd, initialQuery); + + using (SqlConnection srcConn = new SqlConnection(srcConstr)) + using (SqlCommand srcCmd = new SqlCommand(sourceQuery, srcConn)) + { + srcConn.Open(); + using (DbDataReader reader = srcCmd.ExecuteReader()) + { + using (SqlBulkCopy bulkcopy = new SqlBulkCopy( + dstConn, SqlBulkCopyOptions.CheckConstraints, txn)) + { + try + { + bulkcopy.DestinationTableName = dstTable; + bulkcopy.ColumnMappings.Add(0, 2); + bulkcopy.ColumnMappings.Add(2, 0); + bulkcopy.ColumnOrderHints.Add("CustomerID", SortOrder.Ascending); + bulkcopy.ColumnOrderHints.Add("ContactName", SortOrder.Descending); + bulkcopy.WriteToServer(reader); + } + finally + { + txn.Rollback(); + } + } + } + } + } + } + } +} diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/SqlBulkCopyTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/SqlBulkCopyTest.cs index fe9009af12..965dfde9f1 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/SqlBulkCopyTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/SqlBulkCopyTest/SqlBulkCopyTest.cs @@ -247,5 +247,42 @@ public void DestinationTableNameWithSpecialCharTest() { DestinationTableNameWithSpecialChar.Test(srcConstr, AddGuid("SqlBulkCopyTest_DestinationTableNameWithSpecialChar")); } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public void OrderHintTest() + { + OrderHint.Test(srcConstr, AddGuid("SqlBulkCopyTest_OrderHint"), AddGuid("SqlBulkCopyTest_OrderHint2")); + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public void OrderHintAsyncTest() + { + OrderHintAsync.Test(srcConstr, AddGuid("SqlBulkCopyTest_OrderHintAsync"), AddGuid("SqlBulkCopyTest_OrderHintAsync2")); + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public void OrderHintMissingTargetColumnTest() + { + OrderHintMissingTargetColumn.Test(srcConstr, AddGuid("SqlBulkCopyTest_OrderHintMissingTargetColumn")); + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public void OrderHintDuplicateColumnTest() + { + OrderHintDuplicateColumn.Test(srcConstr, AddGuid("SqlBulkCopyTest_OrderHintDuplicateColumn")); + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public void OrderHintTransactionTest() + { + OrderHintTransaction.Test(srcConstr, AddGuid("SqlBulkCopyTest_OrderHintTransaction")); + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + [ActiveIssue(12219)] + public void OrderHintIdentityColumnTest() + { + OrderHintIdentityColumn.Test(srcConstr, AddGuid("SqlBulkCopyTest_OrderHintIdentityColumn")); + } } } From e1b6cd494100171d088d278c9d21810b02a9aa90 Mon Sep 17 00:00:00 2001 From: Karina Zhou Date: Tue, 9 Jun 2020 18:50:09 -0700 Subject: [PATCH 2/5] Add correct code page for Kazakh collation (#584) --- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 3 + .../src/Microsoft/Data/SqlClient/TdsParser.cs | 3 + .../SQL/DataReaderTest/DataReaderTest.cs | 55 +++++++++++++++++++ 3 files changed, 61 insertions(+) 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 eedcf86f8f..87d4999727 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 @@ -4268,6 +4268,9 @@ internal int GetCodePage(SqlCollation collation, TdsParserStateObject stateObj) { } break; + case 0x43f: + codePage = 1251; // Kazakh code page based on SQL Server + break; default: break; } 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 692d3ef85c..930deee878 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 @@ -4730,6 +4730,9 @@ internal int GetCodePage(SqlCollation collation, TdsParserStateObject stateObj) ADP.TraceExceptionWithoutRethrow(e); } break; + case 0x43f: + codePage = 1251; // Kazakh code page based on SQL Server + break; default: break; } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderTest.cs index dca2c07362..b7787b5855 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataReaderTest/DataReaderTest.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Data; using System.Text; +using System.Threading; using Xunit; namespace Microsoft.Data.SqlClient.ManualTesting.Tests @@ -145,6 +146,60 @@ public static void CheckSparseColumnBit() } } + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup), nameof(DataTestUtility.IsNotAzureServer))] + public static void CollatedDataReaderTest() + { + var databaseName = DataTestUtility.GetUniqueName("DB"); + // Remove square brackets + var dbName = databaseName.Substring(1, databaseName.Length - 2); + + SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString) + { + InitialCatalog = dbName, + Pooling = false + }; + + using (SqlConnection con = new SqlConnection(DataTestUtility.TCPConnectionString)) + using (SqlCommand cmd = con.CreateCommand()) + { + try + { + con.Open(); + + // Create collated database + cmd.CommandText = $"CREATE DATABASE {databaseName} COLLATE KAZAKH_90_CI_AI"; + cmd.ExecuteNonQuery(); + + //Create connection without pooling in order to delete database later. + using (SqlConnection dbCon = new SqlConnection(builder.ConnectionString)) + using (SqlCommand dbCmd = dbCon.CreateCommand()) + { + var data = "TestData"; + + dbCon.Open(); + dbCmd.CommandText = $"SELECT '{data}'"; + using (SqlDataReader reader = dbCmd.ExecuteReader()) + { + reader.Read(); + Assert.Equal(data, reader.GetString(0)); + } + } + + // Let connection close safely before dropping database for slow servers. + Thread.Sleep(500); + } + catch (SqlException e) + { + Assert.True(false, $"Unexpected Exception occurred: {e.Message}"); + } + finally + { + cmd.CommandText = $"DROP DATABASE {databaseName}"; + cmd.ExecuteNonQuery(); + } + } + } + private static bool IsColumnBitSet(SqlConnection con, string selectQuery, int indexOfColumnSet) { bool columnSetPresent = false; From 4258e4b44220c5f96997717a9b3441968108abd4 Mon Sep 17 00:00:00 2001 From: Karina Zhou Date: Tue, 9 Jun 2020 18:50:25 -0700 Subject: [PATCH 3/5] Fix | Fix SqlClientMetaDataCollectionNames.Parameters mismatching issue (#580) * Fix Parameters undefined issue and add test case * Update valid names and add tests --- .../SqlClientMetaDataCollectionNames.xml | 18 +++-- .../netcore/ref/Microsoft.Data.SqlClient.cs | 10 ++- .../SqlClientMetaDataCollectionNames.cs | 13 +-- .../netfx/ref/Microsoft.Data.SqlClient.cs | 10 ++- .../SqlClientMetaDataCollectionNames.cs | 13 +-- .../SqlClientMetaDataCollectionNamesTest.cs | 5 +- .../ConnectionSchemaTest.cs | 79 +++++++++++++++++-- 7 files changed, 116 insertions(+), 32 deletions(-) diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlClientMetaDataCollectionNames.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlClientMetaDataCollectionNames.xml index 8aa207b418..e013016ee9 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlClientMetaDataCollectionNames.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlClientMetaDataCollectionNames.xml @@ -24,14 +24,10 @@ A constant for use with the **GetSchema** method that represents the **Indexes** collection. To be added. - - A constant for use with the **GetSchema** method that represents the **Parameters** collection. + + A constant for use with the **GetSchema** method that represents the **ProcedureParameters** collection. To be added. - - - A constant for use with the **GetSchema** method that represents the **ProcedureColumns** collection. - To be added. - + A constant for use with the **GetSchema** method that represents the **Procedures** collection. To be added. @@ -56,5 +52,13 @@ A constant for use with the **GetSchema** method that represents the **Views** collection. To be added. + + A constant for use with the **GetSchema** method that represents the **AllColumns** collection. + To be added. + + + A constant for use with the **GetSchema** method that represents the **ColumnSetColumns** collection. + To be added. + diff --git a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs index 52e6e35dd3..461bee2eaf 100644 --- a/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netcore/ref/Microsoft.Data.SqlClient.cs @@ -328,10 +328,8 @@ public static partial class SqlClientMetaDataCollectionNames public static readonly string IndexColumns; /// public static readonly string Indexes; - /// - public static readonly string Parameters; - /// - public static readonly string ProcedureColumns; + /// + public static readonly string ProcedureParameters; /// public static readonly string Procedures; /// @@ -344,6 +342,10 @@ public static partial class SqlClientMetaDataCollectionNames public static readonly string ViewColumns; /// public static readonly string Views; + /// + public static readonly string AllColumns; + /// + public static readonly string ColumnSetColumns; } /// [System.ComponentModel.DefaultEventAttribute("RecordsAffected")] diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlClientMetaDataCollectionNames.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlClientMetaDataCollectionNames.cs index 7caf4421fa..1c805b53df 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlClientMetaDataCollectionNames.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SqlClientMetaDataCollectionNames.cs @@ -23,11 +23,8 @@ public static class SqlClientMetaDataCollectionNames /// public static readonly string Indexes = "Indexes"; - /// - public static readonly string Parameters = "Parameters"; - - /// - public static readonly string ProcedureColumns = "ProcedureColumns"; + /// + public static readonly string ProcedureParameters = "ProcedureParameters"; /// public static readonly string Procedures = "Procedures"; @@ -46,5 +43,11 @@ public static class SqlClientMetaDataCollectionNames /// public static readonly string Views = "Views"; + + /// + public static readonly string AllColumns = "AllColumns"; // supported starting from SQL Server 2008 + + /// + public static readonly string ColumnSetColumns = "ColumnSetColumns"; // supported starting from SQL Server 2008 } } diff --git a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs index 345ebcea9e..197e886da2 100644 --- a/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs +++ b/src/Microsoft.Data.SqlClient/netfx/ref/Microsoft.Data.SqlClient.cs @@ -365,10 +365,8 @@ public static partial class SqlClientMetaDataCollectionNames public static readonly string IndexColumns; /// public static readonly string Indexes; - /// - public static readonly string Parameters; - /// - public static readonly string ProcedureColumns; + /// + public static readonly string ProcedureParameters; /// public static readonly string Procedures; /// @@ -381,6 +379,10 @@ public static partial class SqlClientMetaDataCollectionNames public static readonly string ViewColumns; /// public static readonly string Views; + /// + public static readonly string AllColumns; + /// + public static readonly string ColumnSetColumns; } /// public sealed partial class SqlClientPermission : System.Data.Common.DBDataPermission diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlClientMetaDataCollectionNames.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlClientMetaDataCollectionNames.cs index eb2db23548..5400ac2fb0 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlClientMetaDataCollectionNames.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/SqlClient/SqlClientMetaDataCollectionNames.cs @@ -22,11 +22,8 @@ public static class SqlClientMetaDataCollectionNames /// public static readonly string Indexes = "Indexes"; - /// - public static readonly string Parameters = "Parameters"; - - /// - public static readonly string ProcedureColumns = "ProcedureColumns"; + /// + public static readonly string ProcedureParameters = "ProcedureParameters"; /// public static readonly string Procedures = "Procedures"; @@ -46,5 +43,11 @@ public static class SqlClientMetaDataCollectionNames /// public static readonly string Views = "Views"; + /// + public static readonly string AllColumns = "AllColumns"; // supported starting from SQL Server 2008 + + /// + public static readonly string ColumnSetColumns = "ColumnSetColumns"; // supported starting from SQL Server 2008 + } } diff --git a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlClientMetaDataCollectionNamesTest.cs b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlClientMetaDataCollectionNamesTest.cs index 69ed6d6304..789949ee68 100644 --- a/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlClientMetaDataCollectionNamesTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/FunctionalTests/SqlClientMetaDataCollectionNamesTest.cs @@ -16,14 +16,15 @@ public void ValuesTest() Assert.Equal("ForeignKeys", SqlClientMetaDataCollectionNames.ForeignKeys); Assert.Equal("IndexColumns", SqlClientMetaDataCollectionNames.IndexColumns); Assert.Equal("Indexes", SqlClientMetaDataCollectionNames.Indexes); - Assert.Equal("Parameters", SqlClientMetaDataCollectionNames.Parameters); - Assert.Equal("ProcedureColumns", SqlClientMetaDataCollectionNames.ProcedureColumns); + Assert.Equal("ProcedureParameters", SqlClientMetaDataCollectionNames.ProcedureParameters); Assert.Equal("Procedures", SqlClientMetaDataCollectionNames.Procedures); Assert.Equal("Tables", SqlClientMetaDataCollectionNames.Tables); Assert.Equal("UserDefinedTypes", SqlClientMetaDataCollectionNames.UserDefinedTypes); Assert.Equal("Users", SqlClientMetaDataCollectionNames.Users); Assert.Equal("ViewColumns", SqlClientMetaDataCollectionNames.ViewColumns); Assert.Equal("Views", SqlClientMetaDataCollectionNames.Views); + Assert.Equal("AllColumns", SqlClientMetaDataCollectionNames.AllColumns); + Assert.Equal("ColumnSetColumns", SqlClientMetaDataCollectionNames.ColumnSetColumns); } } } diff --git a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataBaseSchemaTest/ConnectionSchemaTest.cs b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataBaseSchemaTest/ConnectionSchemaTest.cs index 6e7e1623a2..7b2742e8d6 100644 --- a/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataBaseSchemaTest/ConnectionSchemaTest.cs +++ b/src/Microsoft.Data.SqlClient/tests/ManualTests/SQL/DataBaseSchemaTest/ConnectionSchemaTest.cs @@ -12,17 +12,89 @@ public static class ConnectionSchemaTest { [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] - public static void GetAllTablesFromSchema() + public static void GetTablesFromSchema() { VerifySchemaTable(SqlClientMetaDataCollectionNames.Tables, new string[] { "TABLE_CATALOG", "TABLE_SCHEMA", "TABLE_NAME", "TABLE_TYPE" }); } [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] - public static void GetAllProceduresFromSchema() + public static void GetProceduresFromSchema() { VerifySchemaTable(SqlClientMetaDataCollectionNames.Procedures, new string[] { "ROUTINE_SCHEMA", "ROUTINE_NAME", "ROUTINE_TYPE" }); } + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void GetProcedureParametersFromSchema() + { + VerifySchemaTable(SqlClientMetaDataCollectionNames.ProcedureParameters, new string[] { "PARAMETER_MODE", "PARAMETER_NAME" }); + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void GetDatabasesFromSchema() + { + VerifySchemaTable(SqlClientMetaDataCollectionNames.Databases, new string[] { "database_name", "dbid", "create_date" }); + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void GetForeignKeysFromSchema() + { + VerifySchemaTable(SqlClientMetaDataCollectionNames.ForeignKeys, new string[] { "CONSTRAINT_TYPE", "IS_DEFERRABLE", "INITIALLY_DEFERRED" }); + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void GetIndexesFromSchema() + { + VerifySchemaTable(SqlClientMetaDataCollectionNames.Indexes, new string[] { "index_name", "constraint_name" }); + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void GetIndexColumnsFromSchema() + { + VerifySchemaTable(SqlClientMetaDataCollectionNames.IndexColumns, new string[] { "index_name", "KeyType", "column_name" }); + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void GetColumnsFromSchema() + { + VerifySchemaTable(SqlClientMetaDataCollectionNames.Columns, new string[] { "IS_NULLABLE", "COLUMN_DEFAULT" }); + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void GetAllColumnsFromSchema() + { + VerifySchemaTable(SqlClientMetaDataCollectionNames.AllColumns, new string[] { "IS_NULLABLE", "COLUMN_DEFAULT", "IS_FILESTREAM", "IS_SPARSE", "IS_COLUMN_SET" }); + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void GetColumnSetColumnsFromSchema() + { + VerifySchemaTable(SqlClientMetaDataCollectionNames.ColumnSetColumns, new string[] { "IS_NULLABLE", "COLUMN_DEFAULT", "IS_FILESTREAM", "IS_SPARSE", "IS_COLUMN_SET" }); + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void GetUsersFromSchema() + { + VerifySchemaTable(SqlClientMetaDataCollectionNames.Users, new string[] { "uid", "user_name" }); + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void GetViewsFromSchema() + { + VerifySchemaTable(SqlClientMetaDataCollectionNames.Views, new string[] { "TABLE_NAME", "CHECK_OPTION", "IS_UPDATABLE" }); + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void GetViewColumnsFromSchema() + { + VerifySchemaTable(SqlClientMetaDataCollectionNames.ViewColumns, new string[] { "VIEW_CATALOG", "VIEW_SCHEMA", "VIEW_NAME" }); + } + + [ConditionalFact(typeof(DataTestUtility), nameof(DataTestUtility.AreConnStringsSetup))] + public static void GetUserDefinedTypesFromSchema() + { + VerifySchemaTable(SqlClientMetaDataCollectionNames.UserDefinedTypes, new string[] { "assembly_name", "version_revision", "culture_info" }); + } + private static void VerifySchemaTable(string schemaItemName, string[] testColumnNames) { SqlConnectionStringBuilder builder = new SqlConnectionStringBuilder(DataTestUtility.TCPConnectionString) @@ -36,9 +108,6 @@ private static void VerifySchemaTable(string schemaItemName, string[] testColumn connection.Open(); DataTable table = connection.GetSchema(schemaItemName); - // Display the contents of the table. - Assert.InRange(table.Rows.Count, 1, int.MaxValue); - // Get all table columns HashSet columnNames = new HashSet(); From e601e20ca155b47c971854351bf594756ab95d57 Mon Sep 17 00:00:00 2001 From: DavoudEshtehari <61173489+DavoudEshtehari@users.noreply.github.com> Date: Tue, 9 Jun 2020 18:52:35 -0700 Subject: [PATCH 4/5] Raise warning message for insecure protocols (#591) --- .../SqlClientLogger.xml | 6 ++ .../Interop/SNINativeMethodWrapper.Windows.cs | 2 +- .../Microsoft/Data/SqlClient/SNI/SNIHandle.cs | 4 + .../Data/SqlClient/SNI/SNIMarsConnection.cs | 2 + .../Data/SqlClient/SNI/SNIMarsHandle.cs | 2 + .../Data/SqlClient/SNI/SNINpHandle.cs | 15 ++++ .../Data/SqlClient/SNI/SNITcpHandle.cs | 15 ++++ .../Data/SqlClient/TdsParser.Unix.cs | 3 +- .../Data/SqlClient/TdsParser.Windows.cs | 6 +- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 17 +++- .../Data/SqlClient/TdsParserHelperClasses.cs | 61 ++++++++++++++ .../Data/SqlClient/TdsParserStateObject.cs | 2 +- .../SqlClient/TdsParserStateObjectManaged.cs | 6 +- .../SqlClient/TdsParserStateObjectNative.cs | 65 ++++++++++++++- .../netcore/src/Resources/SR.Designer.cs | 9 ++ .../netcore/src/Resources/SR.resx | 3 + .../Interop/SNINativeManagedWrapperX64.cs | 2 +- .../Interop/SNINativeManagedWrapperX86.cs | 2 +- .../Data/Interop/SNINativeMethodWrapper.cs | 6 +- .../src/Microsoft/Data/SqlClient/TdsParser.cs | 15 +++- .../Data/SqlClient/TdsParserHelperClasses.cs | 83 +++++++++++++++++++ .../netfx/src/Resources/Strings.Designer.cs | 18 ++++ .../netfx/src/Resources/Strings.resx | 6 ++ .../Data/SqlClient/SqlClientLogger.cs | 15 +++- 24 files changed, 344 insertions(+), 21 deletions(-) diff --git a/doc/snippets/Microsoft.Data.SqlClient/SqlClientLogger.xml b/doc/snippets/Microsoft.Data.SqlClient/SqlClientLogger.xml index 229f5eee58..c093bbd505 100644 --- a/doc/snippets/Microsoft.Data.SqlClient/SqlClientLogger.xml +++ b/doc/snippets/Microsoft.Data.SqlClient/SqlClientLogger.xml @@ -37,5 +37,11 @@ Logs information through a specified method of the current instance type. To be added. + + The type to be logged. + The logging method. + The message to be logged. + Logs warning through a specified method of the current instance type. + diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Interop/SNINativeMethodWrapper.Windows.cs b/src/Microsoft.Data.SqlClient/netcore/src/Interop/SNINativeMethodWrapper.Windows.cs index 1296e4afa3..a4eed7751b 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Interop/SNINativeMethodWrapper.Windows.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Interop/SNINativeMethodWrapper.Windows.cs @@ -225,7 +225,7 @@ internal struct SNI_Error internal static extern uint SNITerminate(); [DllImport(SNI, CallingConvention = CallingConvention.Cdecl, EntryPoint = "SNIWaitForSSLHandshakeToCompleteWrapper")] - internal static extern uint SNIWaitForSSLHandshakeToComplete([In] SNIHandle pConn, int dwMilliseconds); + internal static extern uint SNIWaitForSSLHandshakeToComplete([In] SNIHandle pConn, int dwMilliseconds, out uint pProtocolVersion); [DllImport(SNI, CallingConvention = CallingConvention.Cdecl)] internal static extern uint UnmanagedIsTokenRestricted([In] IntPtr token, [MarshalAs(UnmanagedType.Bool)] out bool isRestricted); diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNIHandle.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNIHandle.cs index 6c5bda96b2..bd3facd5fc 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNIHandle.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNIHandle.cs @@ -91,6 +91,10 @@ internal abstract class SNIHandle public abstract void ReturnPacket(SNIPacket packet); + /// + /// Gets a value that indicates the security protocol used to authenticate this connection. + /// + public virtual int ProtocolVersion { get; } = 0; #if DEBUG /// /// Test handle for killing underlying connection diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNIMarsConnection.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNIMarsConnection.cs index f3ed0c1be0..5877508942 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNIMarsConnection.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNIMarsConnection.cs @@ -35,6 +35,8 @@ public Guid ConnectionId } } + public int ProtocolVersion => _lowerHandle.ProtocolVersion; + /// /// Constructor /// diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNIMarsHandle.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNIMarsHandle.cs index 54572634cd..8c35907a8f 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNIMarsHandle.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNIMarsHandle.cs @@ -47,6 +47,8 @@ internal sealed class SNIMarsHandle : SNIHandle public override int ReserveHeaderSize => SNISMUXHeader.HEADER_LENGTH; + public override int ProtocolVersion => _connection.ProtocolVersion; + /// /// Dispose object /// diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNINpHandle.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNINpHandle.cs index ffda6c6248..362d4397d2 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNINpHandle.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNINpHandle.cs @@ -120,6 +120,21 @@ public override uint Status } } + public override int ProtocolVersion + { + get + { + try + { + return (int)_sslStream.SslProtocol; + } + catch + { + return base.ProtocolVersion; + } + } + } + public override uint CheckConnection() { long scopeID = SqlClientEventSource.Log.SNIScopeEnterEvent(""); diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNITcpHandle.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNITcpHandle.cs index 158c13949a..e83e63882a 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNITcpHandle.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/SNI/SNITcpHandle.cs @@ -93,6 +93,21 @@ public override uint Status } } + public override int ProtocolVersion + { + get + { + try + { + return (int)_sslStream.SslProtocol; + } + catch + { + return base.ProtocolVersion; + } + } + } + /// /// Constructor /// diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Unix.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Unix.cs index 73eb6fd93f..1c875f5813 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Unix.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Unix.cs @@ -18,10 +18,9 @@ private void LoadSSPILibrary() // No - Op } - private void WaitForSSLHandShakeToComplete(ref uint error) + private void WaitForSSLHandShakeToComplete(ref uint error, ref int protocolVersion) { // No - Op - } private SNIErrorDetails GetSniErrorDetails() diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Windows.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Windows.cs index 4e60f4e7f6..d5ddb5d47d 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Windows.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParser.Windows.cs @@ -75,15 +75,13 @@ private void LoadSSPILibrary() } } - private void WaitForSSLHandShakeToComplete(ref uint error) + private void WaitForSSLHandShakeToComplete(ref uint error, ref int protocolVersion) { - if (TdsParserStateObjectFactory.UseManagedSNI) - return; // in the case where an async connection is made, encryption is used and Windows Authentication is used, // wait for SSL handshake to complete, so that the SSL context is fully negotiated before we try to use its // Channel Bindings as part of the Windows Authentication context build (SSL handshake must complete // before calling SNISecGenClientContext). - error = _physicalStateObj.WaitForSSLHandShakeToComplete(); + error = _physicalStateObj.WaitForSSLHandShakeToComplete(out protocolVersion); if (error != TdsEnums.SNI_SUCCESS) { _physicalStateObj.AddError(ProcessSNIError(_physicalStateObj)); 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 87d4999727..507b8cc981 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 @@ -10,6 +10,8 @@ using System.Diagnostics; using System.Globalization; using System.IO; +using System.Reflection; +using System.Security.Authentication; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -19,6 +21,7 @@ using Microsoft.Data.SqlClient.DataClassification; using Microsoft.Data.SqlClient.Server; using Microsoft.Data.SqlTypes; +using Newtonsoft.Json.Serialization; namespace Microsoft.Data.SqlClient { @@ -39,6 +42,9 @@ internal struct SNIErrorDetails internal sealed partial class TdsParser { private static int _objectTypeCount; // EventSource counter + private readonly SqlClientLogger _logger = new SqlClientLogger(); + private readonly string _typeName; + internal readonly int _objectID = Interlocked.Increment(ref _objectTypeCount); internal int ObjectID => _objectID; @@ -174,6 +180,7 @@ internal TdsParser(bool MARS, bool fAsynchronous) _physicalStateObj = TdsParserStateObjectFactory.Singleton.CreateTdsParserStateObject(this); DataClassificationVersion = TdsEnums.DATA_CLASSIFICATION_NOT_ENABLED; + _typeName = GetType().Name; } internal SqlInternalConnectionTds Connection @@ -906,7 +913,15 @@ private PreLoginHandshakeStatus ConsumePreLoginHandshake(bool encrypt, bool trus ThrowExceptionAndWarning(_physicalStateObj); } - WaitForSSLHandShakeToComplete(ref error); + int protocolVersion = 0; + WaitForSSLHandShakeToComplete(ref error, ref protocolVersion); + + SslProtocols protocol = (SslProtocols)protocolVersion; + string warningMessage = protocol.GetProtocolWarning(); + if(!string.IsNullOrEmpty(warningMessage)) + { + _logger.LogWarning(_typeName, MethodBase.GetCurrentMethod().Name, warningMessage); + } // create a new packet encryption changes the internal packet size _physicalStateObj.ClearAllWritePackets(); 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 1e24817b85..66c0966e7d 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 @@ -12,6 +12,7 @@ using System.Diagnostics; using System.Globalization; using System.Security; +using System.Security.Authentication; using System.Text; using Microsoft.Data.Common; using Microsoft.Data.SqlTypes; @@ -917,4 +918,64 @@ private void ParseMultipartName() internal static readonly MultiPartTableName Null = new MultiPartTableName(new string[] { null, null, null, null }); } + + internal static class SslProtocolsHelper + { + private static string ToFriendlyName(this SslProtocols protocol) + { + string name; + + /* The SslProtocols.Tls13 is supported by netcoreapp3.1 and later + * This driver does not support this version yet! + if ((protocol & SslProtocols.Tls13) == SslProtocols.Tls13) + { + name = "TLS 1.3"; + }*/ + if((protocol & SslProtocols.Tls12) == SslProtocols.Tls12) + { + name = "TLS 1.2"; + } + else if ((protocol & SslProtocols.Tls11) == SslProtocols.Tls11) + { + name = "TLS 1.1"; + } + else if ((protocol & SslProtocols.Tls) == SslProtocols.Tls) + { + name = "TLS 1.0"; + } +#pragma warning disable CS0618 // Type or member is obsolete: SSL is depricated + else if ((protocol & SslProtocols.Ssl3) == SslProtocols.Ssl3) + { + name = "SSL 3.0"; + } + else if ((protocol & SslProtocols.Ssl2) == SslProtocols.Ssl2) +#pragma warning restore CS0618 // Type or member is obsolete: SSL is depricated + { + name = "SSL 2.0"; + } + else + { + name = protocol.ToString(); + } + + return name; + } + + /// + /// check the negotiated secure protocol if it's under TLS 1.2 + /// + /// + /// Localized warning message + public static string GetProtocolWarning(this SslProtocols protocol) + { + string message = string.Empty; +#pragma warning disable CS0618 // Type or member is obsolete : SSL is depricated + if ((protocol & (SslProtocols.Ssl2 | SslProtocols.Ssl3 | SslProtocols.Tls | SslProtocols.Tls11)) != SslProtocols.None) +#pragma warning restore CS0618 // Type or member is obsolete : SSL is depricated + { + message = SRHelper.Format(SR.SEC_ProtocolWarning, protocol.ToFriendlyName()); + } + return message; + } + } } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs index 9c197b9c37..6888ad0453 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObject.cs @@ -774,7 +774,7 @@ private void ResetCancelAndProcessAttention() internal abstract uint EnableSsl(ref uint info); - internal abstract uint WaitForSSLHandShakeToComplete(); + internal abstract uint WaitForSSLHandShakeToComplete(out int protocolVersion); internal abstract void Dispose(); diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectManaged.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectManaged.cs index 46ae4c4dfb..6e25589986 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectManaged.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectManaged.cs @@ -220,6 +220,10 @@ internal override uint GenerateSspiClientContext(byte[] receivedBuff, uint recei return 0; } - internal override uint WaitForSSLHandShakeToComplete() => 0; + internal override uint WaitForSSLHandShakeToComplete(out int protocolVersion) + { + protocolVersion = Handle.ProtocolVersion; + return 0; + } } } diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs index 97825278ec..a38b5524df 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Microsoft/Data/SqlClient/TdsParserStateObjectNative.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; +using System.Security.Authentication; using System.Threading.Tasks; using Microsoft.Data.Common; @@ -13,6 +14,25 @@ namespace Microsoft.Data.SqlClient { internal class TdsParserStateObjectNative : TdsParserStateObject { + // protocol versions from native sni + [Flags] + private enum NativeProtocols + { + SP_PROT_SSL2_SERVER = 0x00000004, + SP_PROT_SSL2_CLIENT = 0x00000008, + SP_PROT_SSL3_SERVER = 0x00000010, + SP_PROT_SSL3_CLIENT = 0x00000020, + SP_PROT_TLS1_0_SERVER = 0x00000040, + SP_PROT_TLS1_0_CLIENT = 0x00000080, + SP_PROT_TLS1_1_SERVER = 0x00000100, + SP_PROT_TLS1_1_CLIENT = 0x00000200, + SP_PROT_TLS1_2_SERVER = 0x00000400, + SP_PROT_TLS1_2_CLIENT = 0x00000800, + SP_PROT_TLS1_3_SERVER = 0x00001000, + SP_PROT_TLS1_3_CLIENT = 0x00002000, + SP_PROT_NONE = 0x0 + } + private SNIHandle _sessionHandle = null; // the SNI handle we're to work on private SNIPacket _sniPacket = null; // Will have to re-vamp this for MARS @@ -309,8 +329,49 @@ internal override uint SetConnectionBufferSize(ref uint unsignedPacketSize) internal override uint GenerateSspiClientContext(byte[] receivedBuff, uint receivedLength, ref byte[] sendBuff, ref uint sendLength, byte[] _sniSpnBuffer) => SNINativeMethodWrapper.SNISecGenClientContext(Handle, receivedBuff, receivedLength, sendBuff, ref sendLength, _sniSpnBuffer); - internal override uint WaitForSSLHandShakeToComplete() - => SNINativeMethodWrapper.SNIWaitForSSLHandshakeToComplete(Handle, GetTimeoutRemaining()); + internal override uint WaitForSSLHandShakeToComplete(out int protocolVersion) + { + uint returnValue = SNINativeMethodWrapper.SNIWaitForSSLHandshakeToComplete(Handle, GetTimeoutRemaining(), out uint nativeProtocolVersion); + var nativeProtocol = (NativeProtocols)nativeProtocolVersion; + + /* The SslProtocols.Tls13 is supported by netcoreapp3.1 and later + * This driver does not support this version yet! + if (nativeProtocol.HasFlag(NativeProtocols.SP_PROT_TLS1_3_CLIENT) || nativeProtocol.HasFlag(NativeProtocols.SP_PROT_TLS1_3_SERVER)) + { + protocolVersion = (int)SslProtocols.Tls13; + }*/ + if (nativeProtocol.HasFlag(NativeProtocols.SP_PROT_TLS1_2_CLIENT) || nativeProtocol.HasFlag(NativeProtocols.SP_PROT_TLS1_2_SERVER)) + { + protocolVersion = (int)SslProtocols.Tls12; + } + else if (nativeProtocol.HasFlag(NativeProtocols.SP_PROT_TLS1_1_CLIENT) || nativeProtocol.HasFlag(NativeProtocols.SP_PROT_TLS1_1_SERVER)) + { + protocolVersion = (int)SslProtocols.Tls11; + } + else if (nativeProtocol.HasFlag(NativeProtocols.SP_PROT_TLS1_0_CLIENT) || nativeProtocol.HasFlag(NativeProtocols.SP_PROT_TLS1_0_SERVER)) + { + protocolVersion = (int)SslProtocols.Tls; + } + else if (nativeProtocol.HasFlag(NativeProtocols.SP_PROT_SSL3_CLIENT) || nativeProtocol.HasFlag(NativeProtocols.SP_PROT_SSL3_SERVER)) + { +#pragma warning disable CS0618 // Type or member is obsolete : SSL is depricated + protocolVersion = (int)SslProtocols.Ssl3; + } + else if (nativeProtocol.HasFlag(NativeProtocols.SP_PROT_SSL2_CLIENT) || nativeProtocol.HasFlag(NativeProtocols.SP_PROT_SSL2_SERVER)) + { + protocolVersion = (int)SslProtocols.Ssl2; +#pragma warning restore CS0618 // Type or member is obsolete : SSL is depricated + } + else if(nativeProtocol.HasFlag(NativeProtocols.SP_PROT_NONE)) + { + protocolVersion = (int)SslProtocols.None; + } + else + { + throw new ArgumentException(SRHelper.Format(SRHelper.net_invalid_enum, nameof(NativeProtocols)), nameof(NativeProtocols)); + } + return returnValue; + } internal override void DisposePacketCache() { diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.Designer.cs b/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.Designer.cs index fd5175504c..23cbaa4efe 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.Designer.cs +++ b/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.Designer.cs @@ -1428,6 +1428,15 @@ internal class SR { } } + /// + /// Looks up a localized string similar to Security Warning: The negotiated '{0}' is an insecured protocol and is supported for backward compatibility only. The recommended protocol is TLS 1.2 and later.. + /// + internal static string SEC_ProtocolWarning { + get { + return ResourceManager.GetString("SEC_ProtocolWarning", resourceCulture); + } + } + /// /// Looks up a localized string similar to I/O Error detected in read/write operation. /// diff --git a/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.resx b/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.resx index bab71b9600..47d203f9ff 100644 --- a/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.resx +++ b/src/Microsoft.Data.SqlClient/netcore/src/Resources/SR.resx @@ -1860,6 +1860,9 @@ The given value{0} of type {1} from the data source cannot be converted to type {2} for Column {3} [{4}]. + + Security Warning: The negotiated {0} is an insecure protocol and is supported for backward compatibility only. The recommended protocol version is TLS 1.2 and later. + A column order hint cannot have an unspecified sort order. diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeManagedWrapperX64.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeManagedWrapperX64.cs index dd585ededa..edfb5e960f 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeManagedWrapperX64.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeManagedWrapperX64.cs @@ -61,7 +61,7 @@ internal static class SNINativeManagedWrapperX64 internal static extern uint SNITerminate(); [DllImport(SNI, CallingConvention = CallingConvention.Cdecl, EntryPoint = "SNIWaitForSSLHandshakeToCompleteWrapper")] - internal static extern uint SNIWaitForSSLHandshakeToComplete([In] SNIHandle pConn, int dwMilliseconds); + internal static extern uint SNIWaitForSSLHandshakeToComplete([In] SNIHandle pConn, int dwMilliseconds, out uint pProtocolVersion); [DllImport(SNI, CallingConvention = CallingConvention.Cdecl)] internal static extern uint UnmanagedIsTokenRestricted([In] IntPtr token, [MarshalAs(UnmanagedType.Bool)] out bool isRestricted); diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeManagedWrapperX86.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeManagedWrapperX86.cs index 339760c1a2..89c9af997b 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeManagedWrapperX86.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeManagedWrapperX86.cs @@ -61,7 +61,7 @@ internal static class SNINativeManagedWrapperX86 internal static extern uint SNITerminate(); [DllImport(SNI, CallingConvention = CallingConvention.Cdecl, EntryPoint = "SNIWaitForSSLHandshakeToCompleteWrapper")] - internal static extern uint SNIWaitForSSLHandshakeToComplete([In] SNIHandle pConn, int dwMilliseconds); + internal static extern uint SNIWaitForSSLHandshakeToComplete([In] SNIHandle pConn, int dwMilliseconds, out uint pProtocolVersion); [DllImport(SNI, CallingConvention = CallingConvention.Cdecl)] internal static extern uint UnmanagedIsTokenRestricted([In] IntPtr token, [MarshalAs(UnmanagedType.Bool)] out bool isRestricted); diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeMethodWrapper.cs b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeMethodWrapper.cs index 499b29b4d3..fefdeea4b7 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeMethodWrapper.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Microsoft/Data/Interop/SNINativeMethodWrapper.cs @@ -505,11 +505,11 @@ internal static uint SNITerminate() SNINativeManagedWrapperX86.SNITerminate(); } - internal static uint SNIWaitForSSLHandshakeToComplete([In] SNIHandle pConn, int dwMilliseconds) + internal static uint SNIWaitForSSLHandshakeToComplete([In] SNIHandle pConn, int dwMilliseconds, out uint pProtocolVersion) { return s_is64bitProcess ? - SNINativeManagedWrapperX64.SNIWaitForSSLHandshakeToComplete(pConn, dwMilliseconds) : - SNINativeManagedWrapperX86.SNIWaitForSSLHandshakeToComplete(pConn, dwMilliseconds); + SNINativeManagedWrapperX64.SNIWaitForSSLHandshakeToComplete(pConn, dwMilliseconds, out pProtocolVersion) : + SNINativeManagedWrapperX86.SNIWaitForSSLHandshakeToComplete(pConn, dwMilliseconds, out pProtocolVersion); } internal static uint UnmanagedIsTokenRestricted([In] IntPtr token, [MarshalAs(UnmanagedType.Bool)] out bool isRestricted) 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 930deee878..859c8bd3bb 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 @@ -9,6 +9,7 @@ using System.Diagnostics; using System.Globalization; using System.IO; +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Security.Cryptography.X509Certificates; @@ -30,6 +31,9 @@ namespace Microsoft.Data.SqlClient sealed internal class TdsParser { private static int _objectTypeCount; // EventSource Counter + private readonly SqlClientLogger _logger = new SqlClientLogger(); + private readonly string _typeName; + internal readonly int _objectID = System.Threading.Interlocked.Increment(ref _objectTypeCount); static Task completedTask; @@ -300,6 +304,7 @@ internal TdsParser(bool MARS, bool fAsynchronous) _fMARS = MARS; // may change during Connect to pre Yukon servers _physicalStateObj = new TdsParserStateObject(this); DataClassificationVersion = TdsEnums.DATA_CLASSIFICATION_NOT_ENABLED; + _typeName = GetType().Name; } internal SqlInternalConnectionTds Connection @@ -1204,15 +1209,21 @@ private void SendPreLoginHandshake(byte[] instanceName, bool encrypt, bool clien // wait for SSL handshake to complete, so that the SSL context is fully negotiated before we try to use its // Channel Bindings as part of the Windows Authentication context build (SSL handshake must complete // before calling SNISecGenClientContext). - error = SNINativeMethodWrapper.SNIWaitForSSLHandshakeToComplete(_physicalStateObj.Handle, _physicalStateObj.GetTimeoutRemaining()); + error = SNINativeMethodWrapper.SNIWaitForSSLHandshakeToComplete(_physicalStateObj.Handle, _physicalStateObj.GetTimeoutRemaining(), out uint protocolVersion); + if (error != TdsEnums.SNI_SUCCESS) { _physicalStateObj.AddError(ProcessSNIError(_physicalStateObj)); ThrowExceptionAndWarning(_physicalStateObj); } + string warningMessage = SslProtocolsHelper.GetProtocolWarning(protocolVersion); + if (!string.IsNullOrEmpty(warningMessage)) + { + _logger.LogWarning(_typeName, MethodBase.GetCurrentMethod().Name, warningMessage); + } + // Validate server certificate - // if (serverCallback != null) { X509Certificate2 serverCert = null; 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 96eb11c0d2..873aeff914 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 @@ -1421,4 +1421,87 @@ private void ParseMultipartName() internal static readonly MultiPartTableName Null = new MultiPartTableName(new string[] { null, null, null, null }); } + + internal static class SslProtocolsHelper + { + // protocol versions from native sni + [Flags] + private enum NativeProtocols + { + SP_PROT_SSL2_SERVER = 0x00000004, + SP_PROT_SSL2_CLIENT = 0x00000008, + SP_PROT_SSL3_SERVER = 0x00000010, + SP_PROT_SSL3_CLIENT = 0x00000020, + SP_PROT_TLS1_0_SERVER = 0x00000040, + SP_PROT_TLS1_0_CLIENT = 0x00000080, + SP_PROT_TLS1_1_SERVER = 0x00000100, + SP_PROT_TLS1_1_CLIENT = 0x00000200, + SP_PROT_TLS1_2_SERVER = 0x00000400, + SP_PROT_TLS1_2_CLIENT = 0x00000800, + SP_PROT_TLS1_3_SERVER = 0x00001000, + SP_PROT_TLS1_3_CLIENT = 0x00002000, + SP_PROT_SSL2 = SP_PROT_SSL2_SERVER | SP_PROT_SSL2_CLIENT, + SP_PROT_SSL3 = SP_PROT_SSL3_SERVER | SP_PROT_SSL3_CLIENT, + SP_PROT_TLS1_0 = SP_PROT_TLS1_0_SERVER | SP_PROT_TLS1_0_CLIENT, + SP_PROT_TLS1_1 = SP_PROT_TLS1_1_SERVER | SP_PROT_TLS1_1_CLIENT, + SP_PROT_TLS1_2 = SP_PROT_TLS1_2_SERVER | SP_PROT_TLS1_2_CLIENT, + SP_PROT_TLS1_3 = SP_PROT_TLS1_3_SERVER | SP_PROT_TLS1_3_CLIENT, + SP_PROT_NONE = 0x0 + } + + private static string ToFriendlyName(this NativeProtocols protocol) + { + string name; + + if (protocol.HasFlag(NativeProtocols.SP_PROT_TLS1_3_CLIENT) || protocol.HasFlag(NativeProtocols.SP_PROT_TLS1_3_SERVER)) + { + name = "TLS 1.3"; + } + else if (protocol.HasFlag(NativeProtocols.SP_PROT_TLS1_2_CLIENT) || protocol.HasFlag(NativeProtocols.SP_PROT_TLS1_2_SERVER)) + { + name = "TLS 1.2"; + } + else if (protocol.HasFlag(NativeProtocols.SP_PROT_TLS1_1_CLIENT) || protocol.HasFlag(NativeProtocols.SP_PROT_TLS1_1_SERVER)) + { + name = "TLS 1.1"; + } + else if (protocol.HasFlag(NativeProtocols.SP_PROT_TLS1_0_CLIENT) || protocol.HasFlag(NativeProtocols.SP_PROT_TLS1_0_SERVER)) + { + name = "TLS 1.0"; + } + else if (protocol.HasFlag(NativeProtocols.SP_PROT_SSL3_CLIENT) || protocol.HasFlag(NativeProtocols.SP_PROT_SSL3_SERVER)) + { + name = "SSL 3.0"; + } + else if (protocol.HasFlag(NativeProtocols.SP_PROT_SSL2_CLIENT) || protocol.HasFlag(NativeProtocols.SP_PROT_SSL2_SERVER)) + { + name = "SSL 2.0"; + } + else if(protocol.HasFlag(NativeProtocols.SP_PROT_NONE)) + { + name = "None"; + } + else + { + throw new ArgumentException(StringsHelper.GetString(StringsHelper.net_invalid_enum, nameof(NativeProtocols)), nameof(NativeProtocols)); + } + return name; + } + + /// + /// check the negotiated secure protocol if it's under TLS 1.2 + /// + /// + /// Localized warning message + public static string GetProtocolWarning(uint protocol) + { + var nativeProtocol = (NativeProtocols)protocol; + string message = string.Empty; + if ((nativeProtocol & (NativeProtocols.SP_PROT_SSL2 | NativeProtocols.SP_PROT_SSL3 | NativeProtocols.SP_PROT_TLS1_1)) != NativeProtocols.SP_PROT_NONE) + { + message = StringsHelper.GetString(Strings.SEC_ProtocolWarning, nativeProtocol.ToFriendlyName()); + } + return message; + } + } } diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs index 9f40b57bdd..a4ca42aeb4 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs +++ b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.Designer.cs @@ -7062,6 +7062,15 @@ internal class Strings { } } + /// + /// Looks up a localized string similar to The specified value is not valid in the '{0}' enumeration.. + /// + internal static string net_invalid_enum { + get { + return ResourceManager.GetString("net_invalid_enum", resourceCulture); + } + } + /// /// Looks up a localized string similar to DateType column for field '{0}' in schema table is null. DataType must be non-null.. /// @@ -8082,6 +8091,15 @@ internal class Strings { } } + /// + /// Looks up a localized string similar to Security Warning: The negotiated '{0}' is an insecured protocol and is supported for backward compatibility only. The recommended protocol is TLS 1.2 and later.. + /// + internal static string SEC_ProtocolWarning { + get { + return ResourceManager.GetString("SEC_ProtocolWarning", resourceCulture); + } + } + /// /// Looks up a localized string similar to I/O Error detected in read/write operation. /// diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx index 0bca401c21..3cc34302a4 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx +++ b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.resx @@ -4530,6 +4530,12 @@ UDT size must be less than {1}, size: {0} + + Security Warning: The negotiated {0} is an insecure protocol and is supported for backward compatibility only. The recommended protocol version is TLS 1.2 and later. + + + The specified value is not valid in the '{0}' enumeration. + The given column order hint is not valid. diff --git a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientLogger.cs b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientLogger.cs index 5ff2f42e75..68afb07e49 100644 --- a/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientLogger.cs +++ b/src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlClientLogger.cs @@ -2,6 +2,8 @@ // 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; + namespace Microsoft.Data.SqlClient { /// @@ -10,7 +12,8 @@ public class SqlClientLogger internal enum LogLevel { Info = 0, - Error, + Warning, + Error } /// @@ -19,10 +22,18 @@ public void LogInfo(string type, string method, string message) SqlClientEventSource.Log.TraceEvent("{3}", type, method, LogLevel.Info, message); } + /// + public void LogWarning(string type, string method, string message) + { + Console.Out.WriteLine(message); + SqlClientEventSource.Log.TraceEvent("{3}", type, method, LogLevel.Warning, message); + } + /// public void LogError(string type, string method, string message) { - SqlClientEventSource.Log.TraceEvent("{3}", type, method, LogLevel.Info, message); + Console.Out.WriteLine(message); + SqlClientEventSource.Log.TraceEvent("{3}", type, method, LogLevel.Error, message); } /// From 0d1997050ce9aa2fc80ead6bf250f9db9d69cb42 Mon Sep 17 00:00:00 2001 From: SqlClient DevOps Date: Wed, 10 Jun 2020 01:55:54 +0000 Subject: [PATCH 5/5] [Scheduled Run] Localized resource files from OneLocBuild --- .../netfx/src/Resources/Strings.de.resx | 30 +++++++++++++++++++ .../netfx/src/Resources/Strings.es.resx | 30 +++++++++++++++++++ .../netfx/src/Resources/Strings.fr.resx | 30 +++++++++++++++++++ .../netfx/src/Resources/Strings.it.resx | 30 +++++++++++++++++++ .../netfx/src/Resources/Strings.ja.resx | 30 +++++++++++++++++++ .../netfx/src/Resources/Strings.ko.resx | 30 +++++++++++++++++++ .../netfx/src/Resources/Strings.pt-BR.resx | 30 +++++++++++++++++++ .../netfx/src/Resources/Strings.ru.resx | 30 +++++++++++++++++++ .../netfx/src/Resources/Strings.zh-Hans.resx | 30 +++++++++++++++++++ .../netfx/src/Resources/Strings.zh-Hant.resx | 30 +++++++++++++++++++ 10 files changed, 300 insertions(+) diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.de.resx b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.de.resx index 58a66f1782..a25204efdf 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.de.resx +++ b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.de.resx @@ -4530,4 +4530,34 @@ Die UDT-Größe muss kleiner als {1} sein. Größe: {0} + + Security Warning: The negotiated {0} is an insecure protocol and is supported for backward compatibility only. The recommended protocol version is TLS 1.2 and later. + + + The specified value is not valid in the '{0}' enumeration. + + + The given column order hint is not valid. + + + The column '{0}' was specified more than once. + + + The sorted column '{0}' is not valid in the destination table. + + + A column order hint cannot have an unspecified sort order. + + + Unsupported authentication specified in this context: {0} + + + Active Directory Interactive authentication timed out. The user took too long to respond to the authentication request. + + + Cannot use 'Authentication=Active Directory Interactive', if the Credential property has been set. + + + Cannot set the Credential property if 'Authentication=Active Directory Interactive' has been specified in the connection string. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.es.resx b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.es.resx index 4ad3c8946e..06cc7767db 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.es.resx +++ b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.es.resx @@ -4530,4 +4530,34 @@ El tamaño de UDT debe ser menor que {1}; tamaño: {0} + + Security Warning: The negotiated {0} is an insecure protocol and is supported for backward compatibility only. The recommended protocol version is TLS 1.2 and later. + + + The specified value is not valid in the '{0}' enumeration. + + + The given column order hint is not valid. + + + The column '{0}' was specified more than once. + + + The sorted column '{0}' is not valid in the destination table. + + + A column order hint cannot have an unspecified sort order. + + + Unsupported authentication specified in this context: {0} + + + Active Directory Interactive authentication timed out. The user took too long to respond to the authentication request. + + + Cannot use 'Authentication=Active Directory Interactive', if the Credential property has been set. + + + Cannot set the Credential property if 'Authentication=Active Directory Interactive' has been specified in the connection string. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.fr.resx b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.fr.resx index a8ccbee9b1..bd22c8feeb 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.fr.resx +++ b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.fr.resx @@ -4530,4 +4530,34 @@ La taille UDT doit être inférieure à {1}, taille : {0} + + Security Warning: The negotiated {0} is an insecure protocol and is supported for backward compatibility only. The recommended protocol version is TLS 1.2 and later. + + + The specified value is not valid in the '{0}' enumeration. + + + The given column order hint is not valid. + + + The column '{0}' was specified more than once. + + + The sorted column '{0}' is not valid in the destination table. + + + A column order hint cannot have an unspecified sort order. + + + Unsupported authentication specified in this context: {0} + + + Active Directory Interactive authentication timed out. The user took too long to respond to the authentication request. + + + Cannot use 'Authentication=Active Directory Interactive', if the Credential property has been set. + + + Cannot set the Credential property if 'Authentication=Active Directory Interactive' has been specified in the connection string. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.it.resx b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.it.resx index f0b497b0d7..12c60c0209 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.it.resx +++ b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.it.resx @@ -4530,4 +4530,34 @@ Le dimensioni del tipo definito dall'utente (UDT) devono essere inferiori a {1}, dimensioni: {0} + + Security Warning: The negotiated {0} is an insecure protocol and is supported for backward compatibility only. The recommended protocol version is TLS 1.2 and later. + + + The specified value is not valid in the '{0}' enumeration. + + + The given column order hint is not valid. + + + The column '{0}' was specified more than once. + + + The sorted column '{0}' is not valid in the destination table. + + + A column order hint cannot have an unspecified sort order. + + + Unsupported authentication specified in this context: {0} + + + Active Directory Interactive authentication timed out. The user took too long to respond to the authentication request. + + + Cannot use 'Authentication=Active Directory Interactive', if the Credential property has been set. + + + Cannot set the Credential property if 'Authentication=Active Directory Interactive' has been specified in the connection string. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.ja.resx b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.ja.resx index daaed69a9f..544bcdb26c 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.ja.resx +++ b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.ja.resx @@ -4530,4 +4530,34 @@ UDT のサイズは {1} より小さくなければなりません。サイズ: {0} + + Security Warning: The negotiated {0} is an insecure protocol and is supported for backward compatibility only. The recommended protocol version is TLS 1.2 and later. + + + The specified value is not valid in the '{0}' enumeration. + + + The given column order hint is not valid. + + + The column '{0}' was specified more than once. + + + The sorted column '{0}' is not valid in the destination table. + + + A column order hint cannot have an unspecified sort order. + + + Unsupported authentication specified in this context: {0} + + + Active Directory Interactive authentication timed out. The user took too long to respond to the authentication request. + + + Cannot use 'Authentication=Active Directory Interactive', if the Credential property has been set. + + + Cannot set the Credential property if 'Authentication=Active Directory Interactive' has been specified in the connection string. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.ko.resx b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.ko.resx index b943f50402..5da87b350d 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.ko.resx +++ b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.ko.resx @@ -4530,4 +4530,34 @@ UDT 크기는 {1}보다 작아야 합니다. 크기: {0} + + Security Warning: The negotiated {0} is an insecure protocol and is supported for backward compatibility only. The recommended protocol version is TLS 1.2 and later. + + + The specified value is not valid in the '{0}' enumeration. + + + The given column order hint is not valid. + + + The column '{0}' was specified more than once. + + + The sorted column '{0}' is not valid in the destination table. + + + A column order hint cannot have an unspecified sort order. + + + Unsupported authentication specified in this context: {0} + + + Active Directory Interactive authentication timed out. The user took too long to respond to the authentication request. + + + Cannot use 'Authentication=Active Directory Interactive', if the Credential property has been set. + + + Cannot set the Credential property if 'Authentication=Active Directory Interactive' has been specified in the connection string. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.pt-BR.resx b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.pt-BR.resx index 328c0665ff..beeb608a90 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.pt-BR.resx +++ b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.pt-BR.resx @@ -4530,4 +4530,34 @@ O tamanho do UDT precisa ser menor que {1}. Tamanho: {0} + + Security Warning: The negotiated {0} is an insecure protocol and is supported for backward compatibility only. The recommended protocol version is TLS 1.2 and later. + + + The specified value is not valid in the '{0}' enumeration. + + + The given column order hint is not valid. + + + The column '{0}' was specified more than once. + + + The sorted column '{0}' is not valid in the destination table. + + + A column order hint cannot have an unspecified sort order. + + + Unsupported authentication specified in this context: {0} + + + Active Directory Interactive authentication timed out. The user took too long to respond to the authentication request. + + + Cannot use 'Authentication=Active Directory Interactive', if the Credential property has been set. + + + Cannot set the Credential property if 'Authentication=Active Directory Interactive' has been specified in the connection string. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.ru.resx b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.ru.resx index d76623d14d..335adb907c 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.ru.resx +++ b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.ru.resx @@ -4530,4 +4530,34 @@ Размер пользовательского типа должен быть меньше {1}, текущий размер: {0}. + + Security Warning: The negotiated {0} is an insecure protocol and is supported for backward compatibility only. The recommended protocol version is TLS 1.2 and later. + + + The specified value is not valid in the '{0}' enumeration. + + + The given column order hint is not valid. + + + The column '{0}' was specified more than once. + + + The sorted column '{0}' is not valid in the destination table. + + + A column order hint cannot have an unspecified sort order. + + + Unsupported authentication specified in this context: {0} + + + Active Directory Interactive authentication timed out. The user took too long to respond to the authentication request. + + + Cannot use 'Authentication=Active Directory Interactive', if the Credential property has been set. + + + Cannot set the Credential property if 'Authentication=Active Directory Interactive' has been specified in the connection string. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.zh-Hans.resx b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.zh-Hans.resx index a28628a017..7fe1f2eaf0 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.zh-Hans.resx +++ b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.zh-Hans.resx @@ -4530,4 +4530,34 @@ UDT 大小必须小于 {1},大小为 {0} + + Security Warning: The negotiated {0} is an insecure protocol and is supported for backward compatibility only. The recommended protocol version is TLS 1.2 and later. + + + The specified value is not valid in the '{0}' enumeration. + + + The given column order hint is not valid. + + + The column '{0}' was specified more than once. + + + The sorted column '{0}' is not valid in the destination table. + + + A column order hint cannot have an unspecified sort order. + + + Unsupported authentication specified in this context: {0} + + + Active Directory Interactive authentication timed out. The user took too long to respond to the authentication request. + + + Cannot use 'Authentication=Active Directory Interactive', if the Credential property has been set. + + + Cannot set the Credential property if 'Authentication=Active Directory Interactive' has been specified in the connection string. + \ No newline at end of file diff --git a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.zh-Hant.resx b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.zh-Hant.resx index 0cc42a06b7..67e644c8a4 100644 --- a/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.zh-Hant.resx +++ b/src/Microsoft.Data.SqlClient/netfx/src/Resources/Strings.zh-Hant.resx @@ -4530,4 +4530,34 @@ UDT 大小必須小於 {1},大小: {0} + + Security Warning: The negotiated {0} is an insecure protocol and is supported for backward compatibility only. The recommended protocol version is TLS 1.2 and later. + + + The specified value is not valid in the '{0}' enumeration. + + + The given column order hint is not valid. + + + The column '{0}' was specified more than once. + + + The sorted column '{0}' is not valid in the destination table. + + + A column order hint cannot have an unspecified sort order. + + + Unsupported authentication specified in this context: {0} + + + Active Directory Interactive authentication timed out. The user took too long to respond to the authentication request. + + + Cannot use 'Authentication=Active Directory Interactive', if the Credential property has been set. + + + Cannot set the Credential property if 'Authentication=Active Directory Interactive' has been specified in the connection string. + \ No newline at end of file