From 53a9e9fc48909aa690f8bde94cb3842ce355ffb9 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] 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 ebbdc7ddb0..856ce9d204 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 SqlBulkCopy(string connectionString, Microsoft.Data.SqlClient.SqlBulkCopy 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 void Remove(Microsoft.Data.SqlClient.SqlBulkCopyColumnMapping value) { } /// 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 900f99b0fb..4328cfeebc 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 static string SQL_BulkLoadInvalidOperationInsideEvent { } } + /// + /// 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 static string SQL_BulkLoadNotAllowDBNull { } } + /// + /// 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 static string SQL_BulkLoadStringTooLong { } } + /// + /// 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 c8f4338941..205b696b80 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 SqlBulkCopy(string connectionString, Microsoft.Data.SqlClient.SqlBulkCopy 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 void Remove(Microsoft.Data.SqlClient.SqlBulkCopyColumnMapping value) { } /// 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 b3b7abecfe..f90f36784d 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 static string SQL_BulkLoadInvalidOperationInsideEvent { } } + /// + /// 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 static string SQL_BulkLoadNotAllowDBNull { } } + /// + /// 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 static string SQL_BulkLoadStringTooLong { } } + /// + /// 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 9e03a7792a..34bd1e6ead 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")); + } } }