dotnet / dotnet/SqlClient

SqlBulkCopy throws inconsistent exceptions depending on the target column datatype.

Open
#1,274 5 comments 0 reactions 0 assignees View on GitHub
Repro Available :heavy_check_mark:
Dominant language
C#
Stars
989
Forks
340
Avg merge
4d 19h
Merged PRs (30d)
72

Description

### Describe the bug
`SqlBulkCopy` throws inconsistent exceptions depending on the target datatype. Not only are the messages different, but the exception types are different as well.
When the target datatype is `nvarchar` for truncation issues you get a `InvalidOperationException`, but if the type is `varchar` you get a completely unhelpful `SqlException`.

varchar

```
Microsoft.Data.SqlClient.SqlException (0x80131904): Received an invalid column length from the bcp client for colid 1.
at Microsoft.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
at Microsoft.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action`1 wrapCloseInAction)
at Microsoft.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, Boolean callerHasConnectionLock, Boolean asyncClose)
at Microsoft.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
at Microsoft.Data.SqlClient.TdsParser.Run(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj)
at Microsoft.Data.SqlClient.SqlBulkCopy.RunParser(BulkCopySimpleResultSet bulkCopyHandler)
at Microsoft.Data.SqlClient.SqlBulkCopy.CopyBatchesAsyncContinuedOnSuccess(BulkCopySimpleResultSet internalResults, String updateBulkCommandText, CancellationToken cts, TaskCompletionSource`1 source)
at Microsoft.Data.SqlClient.SqlBulkCopy.CopyBatchesAsyncContinued(BulkCopySimpleResultSet internalResults, String updateBulkCommandText, CancellationToken cts, TaskCompletionSource`1 source)
at Microsoft.Data.SqlClient.SqlBulkCopy.CopyBatchesAsync(BulkCopySimpleResultSet internalResults, String updateBulkCommandText, CancellationToken cts, TaskCompletionSource`1 source)
at Microsoft.Data.SqlClient.SqlBulkCopy.WriteToServerInternalRestContinuedAsync(BulkCopySimpleResultSet internalResults, CancellationToken cts, TaskCompletionSource`1 source)
at Microsoft.Data.SqlClient.SqlBulkCopy.WriteToServerInternalRestAsync(CancellationToken cts, TaskCompletionSource`1 source)
at Microsoft.Data.SqlClient.SqlBulkCopy.WriteToServerInternalAsync(CancellationToken ctoken)
at Microsoft.Data.SqlClient.SqlBulkCopy.WriteRowSourceToServerAsync(Int32 columnCount, CancellationToken ctoken)
at Microsoft.Data.SqlClient.SqlBulkCopy.WriteToServer(DataTable table, DataRowState rowState)
at Microsoft.Data.SqlClient.SqlBulkCopy.WriteToServer(DataTable table)
at Program.Main() in C:\Users\Michael\source\repos\ConsoleApp1\ConsoleApp1\Program.cs:line 27
```

nvarchar

```
System.InvalidOperationException: The given value '12' of type String from the data source cannot be converted to type nvarchar for Column 1 [b] Row 1.
---> System.InvalidOperationException: String or binary data would be truncated in table '#t', column 'b'. Truncated value: '1'.
at Microsoft.Data.SqlClient.SqlBulkCopy.ConvertValue(Object value, _SqlMetaData metadata, Boolean isNull, Boolean& isSqlType, Boolean& coercedToDataFeed)
--- End of inner exception stack trace ---
at Microsoft.Data.SqlClient.SqlBulkCopy.ConvertValue(Object value, _SqlMetaData metadata, Boolean isNull, Boolean& isSqlType, Boolean& coercedToDataFeed)
at Microsoft.Data.SqlClient.SqlBulkCopy.ReadWriteColumnValueAsync(Int32 col)
at Microsoft.Data.SqlClient.SqlBulkCopy.CopyColumnsAsync(Int32 col, TaskCompletionSource`1 source)
at Microsoft.Data.SqlClient.SqlBulkCopy.CopyRowsAsync(Int32 rowsSoFar, Int32 totalRows, CancellationToken cts, TaskCompletionSource`1 source)
at Microsoft.Data.SqlClient.SqlBulkCopy.CopyBatchesAsyncContinued(BulkCopySimpleResultSet internalResults, String updateBulkCommandText, CancellationToken cts, TaskCompletionSource`1 source)
at Microsoft.Data.SqlClient.SqlBulkCopy.CopyBatchesAsync(BulkCopySimpleResultSet internalResults, String updateBulkCommandText, CancellationToken cts, TaskCompletionSource`1 source)
at Microsoft.Data.SqlClient.SqlBulkCopy.WriteToServerInternalRestContinuedAsync(BulkCopySimpleResultSet internalResults, CancellationToken cts, TaskCompletionSource`1 source)
at Microsoft.Data.SqlClient.SqlBulkCopy.WriteToServerInternalRestAsync(CancellationToken cts, TaskCompletionSource`1 source)
at Microsoft.Data.SqlClient.SqlBulkCopy.WriteToServerInternalAsync(CancellationToken ctoken)
at Microsoft.Data.SqlClient.SqlBulkCopy.WriteRowSourceToServerAsync(Int32 columnCount, CancellationToken ctoken)
at Microsoft.Data.SqlClient.SqlBulkCopy.WriteToServer(DataTable table, DataRowState rowState)
at Microsoft.Data.SqlClient.SqlBulkCopy.WriteToServer(DataTable table)
at Program.Main() in C:\Users\Michael\source\repos\ConsoleApp1\ConsoleApp1\Program.cs:line 38
```

This repro is rather minimal, but in my usual use case I'm dealing with ETLs of dozens of columns. Trying to figure out which column is the offending one that is being truncated and why is time consuming and it is much easier when I know the actual datatype and how long the string is. (Usually I don't get row number as sqlbulkcopy only gives you that for DataTable sources. Although, I really think it would be a nice addition for `DbDataReader` sources).

Repro

```cs
using System.Data;
using Microsoft.Data.SqlClient;
public class Program
{
public static void Main()
{
var localdb = @"Data Source=(localDb)\.;Initial Catalog =Master;Integrated Security=true;";
using var table = new DataTable
{
Columns = { new DataColumn("a", typeof(string)) },
Rows = { "12" }
};
using var conn = new SqlConnection(localdb);
conn.Open();
using var cmd = new SqlCommand("create table #t(a varchar(1), b nvarchar(1))", conn);
cmd.ExecuteNonQuery();
using var bulkCopy = new SqlBulkCopy(conn)
{
DestinationTableName = "#t",
ColumnMappings = { new("a", "a") },
};
try
{
bulkCopy.WriteToServer(table);
}
catch(SqlException e)
{
Console.WriteLine($"varchar:");
Console.WriteLine(e);
}
try
{
bulkCopy.ColumnMappings.Clear();
bulkCopy.ColumnMappings.Add(new("a", "b"));
bulkCopy.WriteToServer(table);
}
catch(InvalidOperationException e)
{
Console.WriteLine($"nvarchar:");
Console.WriteLine(e);
}
}
}
```

### Expected behavior
I would expect the error message would be the same datatype and have the same exact error message modulo the part where it says "nvarchar" replaced with "varchar".

### Further technical details
Microsoft.Data.SqlClient version: 3.0.0
.NET target: 5.0.4
SQL Server version: Sql Server 2019 localdb (but irrelevant)
Operating system: Windows 10 21H1

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.