sql-serversql-server-2022

sqlBulkCopy WriteToServer throwing an error on computed fields (even if they are not included in the INSERT statement)


Moving data from SQL Anywhere to SQL Server and after a few hiccups it was going well until I got the error

The column "A_Future" cannot be modified because it is either a computed column or is the result of a UNION'

Fair enough I was inserting data from SELECT * -- so used this to return only columns that were not calculated.

 'Remove computed columns from the SELECT query
 Dim DR() As DataRow = MSSQLDT.Select("Computed = 'N'")
 Dim vSelectedRows As Integer = DR.Count
 Dim vCurrentRow As Integer = 0
 strSQL = "SELECT "
 For Each Row In DR
 strSQL += Row("Name")
 vCurrentRow += 1
 If vCurrentRow = vSelectedRows Then
     strSQL += " "
 Else
     strSQL += ", "
 End If
Next
strSQL += "FROM " & vTable

This returns the following query string...

SELECT Transaction_ID, Debit, Credit, Paid, P_Description, Document_Date, Supplier_ID, Nominal_Transaction, HOA_Code, Document_Saved, Document_ID, Document_No, Supplier_Inv_No, Type, Paid_Date, Part_Paid, Open_Editing, Editing_Name, Updated_Name, Updated, Reserve_Item, Hold, eCheck_Pending, eSig_Required, eSigOne_ID, eSigTwo_ID FROM A_Purchase_Ledger

.. and it doesn't include any calculated columns. Ran it again, but it still threw the same error (for all calculated columns)

Microsoft.Data.SqlClient.SqlException (0x80131904): The column "A_Future" cannot be modified because it is either a computed column or is the result of a UNION operator.
The column "A_Current" cannot be modified because it is either a computed column or is the result of a UNION operator.
The column "A_30" cannot be modified because it is either a computed column or is the result of a UNION operator.
The column "A_60" cannot be modified because it is either a computed column or is the result of a UNION operator.
The column "A_90" cannot be modified because it is either a computed column or is the result of a UNION operator.
The column "A_Older" cannot be modified because it is either a computed column or is the result of a UNION operator.

This now makes no sense since I am not trying to insert data into them, unless there is a quirk with SqlBulkCopy I am not aware of?

The SELECT Data is added to a DataTable then it goes to this function

  Public Function BulkUpdate_DataMS(DT As DataTable, HOAID As Integer, IsHASoftware As Boolean, TableName As String) As Boolean
  Try
      Using Conn As New Microsoft.Data.SqlClient.SqlConnection
          If IsHASoftware = True Then
              Conn.ConnectionString = HASConString
          Else
              Conn.ConnectionString = ReturnConnStringMS(HOAID)
          End If
          Conn.Open()
          Using vTrans = Conn.BeginTransaction
              Using vBulk As New Microsoft.Data.SqlClient.SqlBulkCopy(Conn, Microsoft.Data.SqlClient.SqlBulkCopyOptions.Default, vTrans)
                  vBulk.DestinationTableName = TableName
                  vBulk.WriteToServer(DT)
              End Using
              vTrans.Commit()

          End Using
          Conn.Close()

      End Using
      Return True
  Catch ex As Exception
      EmailError(ex, 222, PageName)
      Return False
  End Try
 End Function

Solution

  • You need to set the ColumnMappings otherwise it's just going to do an ordinal match.

    Also there is no need for a transaction, as the bulk insert will use its own transaction. And if you have a Using you don't need to close the connection manually.

    Public Function BulkUpdate_DataMS(DT As DataTable, HOAID As Integer, IsHASoftware As Boolean, TableName As String) As Boolean
        Try
            Using Conn As New SqlConnection(If(IsHASoftware, HASConString, ReturnConnStringMS(HOAID)))
                Using vBulk As New SqlBulkCopy(Conn, SqlBulkCopyOptions.Default)
                    vBulk.DestinationTableName = TableName
                    For Each col As DataColumn in DT.Columns
                        vBulk.ColumnMappings.Add(col.Name, col.Name)
                    Next
                    Conn.Open()
                    vBulk.WriteToServer(DT)
                End Using
            End Using
            Return True
        Catch ex As Exception
            EmailError(ex, 222, PageName)
            Return False
        End Try
    End Function
    

    You should probably also consider using Async and Await for all this.

    The correct way to do async wouldn't be to use Task.Run, instead use Await directly on the bulk insert within a function marked as Async

    Public Async Function BulkUpdate_DataMS(DT As DataTable, HOAID As Integer, IsHASoftware As Boolean, TableName As String) As Task(Of Boolean)
        Try
            Using Conn As New SqlConnection(If(IsHASoftware, HASConString, ReturnConnStringMS(HOAID)))
                Using vBulk As New SqlBulkCopy(Conn, SqlBulkCopyOptions.Default)
                    vBulk.DestinationTableName = TableName
                    For Each col As DataColumn in DT.Columns
                        vBulk.ColumnMappings.Add(col.Name, col.Name)
                    Next
                    Await Conn.OpenAsync()    ' NOTE the async
                    Await vBulk.WriteToServerAsync(DT)
                End Using
            End Using
            Return True
        Catch ex As Exception
            EmailError(ex, 222, PageName)
            Return False
        End Try
    End Function
    

    Also if you are reading the data from another server, you don't need to save it in a DataTable at all. You can pass its DbDataReader that you get from ExecuteReader/ExecuteReaderAsync directly to WriteToServer/WriteToServerAsync.