1

I promised myself I would not post this because I have this delusional thought that I am too good of a programmer, yet here we are.

I have altered what I posted earlier last week trying to figure out how to write a VBA function that would write data from an Excel Range to an MS SQL Table. That worked.

Towards the end of the program, I do not know how to construct the final execution of the code; I have tried everything from using the Command.Text in the upper levels, setting it to a Recordset, then executing the recordset, but nothing will make the little VBA troll happy. Here is what I currently have written:

Sub Connection()
    Dim Tbl As String

    Dim InsertQuery As New ADODB.Command
    InsertQuery.CommandType = adCmdText

    Dim xlRow As Long, xlCol As Integer
    Dim DBconnection As New ADODB.Connection
    Dim ConnString As String
    Dim rst As New ADODB.Recordset
    Dim a As Integer, sFieldName As String
    Dim db As DAO.Database
    Dim CurrentDb As Database
    Dim ConnectionStr

    ConnectionStr = "Provider=sqloledb;Server="";Inital Catalog="";Integrated Security=SSPI;User ID="";Password="""

    DBconnection.Open ConnectionStr

    xlRow = 1   'only one row being used *as of now*, and that is the top row in the excel sheet
    xlCol = 119 'First column of misc. data
    While Cells(xlRow, xlCol) <> ""
        If LH = True Then
            Tbl = "Info.CaseBLH"
            InsertQuery.CommandText = "INSERT INTO " & Tbl & " VALUES('"                
        ElseIf RH = True Then
            Tbl = "Info.CaseBRH"
            InsertQuery.CommandText = "INSERT INTO " & Tbl & " VALUES('"
        Else
            MsgBox ("No available sheets")
            'Application.Quit        
        End If

        NK21Data.TableDefs(Tbl).Fields.Count

        For a = 1 To Fields.Count - 1
            'For xlCol = 119 To 230 'columns DO1 to HV1
            Fields.Item(a) = Replace(Cells(xlRow, xlCol), "'", "''") & "', '" 'Includes mitigation for apostrophes in the data

            If Cells(xlRow, xlCol) = "" Then
                rst.Fields.Item(a) = "NULL"
            End If

            xlCol = xlCol + 1
        Next a
        a = a + 1

        Fields.Item(a) = (Format(Now(), "M/D/YYYY") & "')" & vbCrLf)
    Wend

    'On Error GoTo ErrorHandler
    DBconnection.Execute (InsertQuery.CommandText)

    DBconnection.Close
    Set DBconnection = Nothing

    ErrorHandler:
    If Err.Number <> 0 Then
       Msg = "Error # " & Str(Err.Number) & " was generated by " _
       & Err.Source & Chr(13) & "Error Line: " & Erl & Chr(13) & Err.Description
       MsgBox Msg, , "Error", Err.HelpFile, Err.HelpContext
    End If
End Sub

The error I get is:

Command text was not set for the command object.

This error occurs at:

DBconnection.Execute (InsertQuery.CommandText)

If I try using the following:

InsertQuery = DBconnection.Execute

I will get the following error:

Argument not optional

I've been at this for about (give or take) three days and I'm now having nightmares about it so if someone can help me figure out what to do for this I would greatly appreciate it.

GSerg
  • 76,472
  • 17
  • 159
  • 346
Noah Thomas
  • 71
  • 1
  • 9
  • 4
    Is part of your code missing? I can't see where you finish your InsertCommand off. It looks like you leave it hanging at `INSERT INTO table VALUES(` – Caius Jard Nov 19 '19 at 17:18
  • Anyhoo; this code looks like it's running the risk of making a massive ass of things because it's not using parameters. See https://stackoverflow.com/questions/10352211/vba-ado-connection-and-query-parameters - if you do it that way (properly) then you wont have to worry about "if the data contains apostrophes" - never concatenate a value into an SQL string. Concatenate a parameter placeholder in and add a value for the parameter, but never concat the value in – Caius Jard Nov 19 '19 at 17:19
  • @CaiusJard even though I'm trying to insert, will setting it to a parameter still work? What I'm trying to do is pretty much the equivalent of a unicorn and I haven't found hardly any resources of writing data to a database from an Excel table. – Noah Thomas Nov 19 '19 at 17:28
  • @CaiusJard so if I'm gathering, you're asking me to set each xlCol and xlRow to a parameter then concatenate that in? – Noah Thomas Nov 19 '19 at 17:29
  • 1
    Every SQL statement you write, from now until you retire, should be parameterized if it takes input from a source you don't control :) – Caius Jard Nov 19 '19 at 17:29
  • @CaiusJard I apologize for the cringe-worthy code. I'm not a native VBA developer; mostly NC and LadderLogic (siemens, omron, AB) – Noah Thomas Nov 19 '19 at 17:30
  • Lol, no apologies needed - i'm not a VBA dev either, so it might be blind leading the blind - but I'm dragging up old VB6 knowledge from decades ago and referencing other answers with high votes that have VBA in them/MSDN etc so i think it's nearly there. I'm sure someone will comment if I've made a glaring mess, but not much has changed conceptually with ADO - the process of "prepare sql command, set param values, execute, set new values, execute ad infinitum" still holds today – Caius Jard Nov 19 '19 at 18:33
  • By the way, if you're using some DB like SQL Server, you can get SQLServer to connect to the Excel file and read the values into a table in a single command; check out OPENROWSET or OPENDATASOURCE - examples at the bottom of https://learn.microsoft.com/en-us/sql/t-sql/functions/openrowset-transact-sql?view=sql-server-ver15 - something like `SELECT * INTO myNewTempTable FROM OPENROWSET('Microsoft.Jet.OLEDB.4.0', 'C:\temp\myexceldoc.xls;blah;blah''', SheetNameHere); would bring the whole sheet into a new temp table. Google for OPENROWSET excel or OPENDATASOURCE excel – Caius Jard Nov 19 '19 at 18:35

4 Answers4

2

I fixed up and cleaned the code from my earlier answer, tested it to work:

enter image description here

Here's the code:

Option Explicit


Sub DoItThen()

    Dim i As Integer, sqlIns As String, sqlVals As String
    Dim InsertQuery As New ADODB.Command
    Dim firstRow As Long, firstCol As Integer, lastCol As Integer, currRow As Integer
    Dim DBconnection As New ADODB.Connection
    Dim ConnString As String

    ConnString = "Provider=SQLOLEDB.1;Integrated Security=SSPI;Persist Security Info=False;Initial Catalog=Example;Data Source=MYMACHINENAME"

    DBconnection.Open ConnString

    InsertQuery.ActiveConnection = DBconnection
    InsertQuery.CommandType = adCmdText

    ''build the command text side by side, named columns and values with param placeholders
    sqlIns = "INSERT INTO person("
    sqlVals = " VALUES("

    ''i could work these out by scanning the sheet i suppose. hardcoded for now
    firstRow = 2
    firstCol = 3
    lastCol = 5

    ''generate the SQL - its this that lets the column names come in any order in the sheet
    For i = firstCol To lastCol
        sqlIns = sqlIns & Cells(firstRow, i) & ","
        sqlVals = sqlVals & "?,"
        InsertQuery.Parameters.Append InsertQuery.CreateParameter("p" & i - firstCol, adVarChar, adParamInput, 255)
    Next i

    ''chop off the extra trailing commas and form a syntax correct command
    InsertQuery.CommandText = Left$(sqlIns, Len(sqlIns) - 1) & ")" & Left$(sqlVals, Len(sqlVals) - 1) & ")"

    ''iterate the data part of the sheet and execute the query repeatedlty
    currRow = firstRow + 1
    While Cells(currRow, firstCol) <> ""

       For i = firstCol To lastCol
           InsertQuery.Parameters("p" & i - firstCol).Value = Cells(currRow, i)
       Next i

       InsertQuery.Execute , , adExecuteNoRecords ''dont return a resultset

       currRow = currRow + 1
    Wend


    DBconnection.Close
    Set DBconnection = Nothing

ErrorHandler:
    If Err.Number <> 0 Then
       MsgBox Err.Description
    End If
End Sub

It takes the first row as the names of the columns in the db table - any order is fine

It builds a command and populates the parameters

It repeatedly fills the values and executes the query, populating the table

Caius Jard
  • 72,509
  • 5
  • 49
  • 80
  • I tried this and it fared better than my own code, but I'm having an issue with the execute line (see my answer below). – Noah Thomas Nov 20 '19 at 12:41
1

OK; don't shoot me - I'm no VBA whizz but I'm saying you should strive to make your code more like this:

Sub DoItThen()

    Dim a As Integer, sql as String
    Dim InsertQuery As New ADODB.Command

    Dim xlRow As Long, xlCol As Integer
    Dim DBconnection As New ADODB.Connection
    Dim ConnString As String

    ConnString = "Provider=sqloledb;Server="";Inital Catalog="";Integrated Security=SSPI;User ID="";Password="""

    DBconnection.Open ConnString 

    InsertQuery.ActiveConnection = conn
    InsertQuery.CommandType = adCmdText

    If LH = True Then
        sql = "INSERT INTO Info.CaseBLH VALUES(@p1"                
    ElseIf RH = True Then
        sql = "INSERT INTO Info.CaseBRH VALUES(@p1"
    Else
        MsgBox ("No available sheets")
        'Application.Quit        
    End If

    ''does this do anything? I don't know
    NK21Data.TableDefs(Tbl).Fields.Count


    ''let us add some placeholders to the command: we add count-2 because we already have one ? in the command
    ''ps; don't know where you got fields.count from
    For a = 2 To Fields.Count - 1
        sql = sql & ",@p" & a
    Next a

    ''finish off our command
    InsertQuery.CommandText = sql & ")"


    ''now we have a command like INSERT INTO tbl VALUES(@p1, @p2, @p3.."
    ''and setting the command text might pre-populate the parameters collection
    ''with the same number of parameters as are in the command, so let's clear it and 
    ''add the parameters again ourselves so we can control the type

    InsertQuery.Parameters.Clear

    ''create a load of parameters
    For a = 1 To Fields.Count - 1

       InsertQuery.Parameters.Append InsertQuery.CreateParameter("@p" & a, adVarChar, adParamInput, 255) 'adjust if you have strings longer than 255

    Next a

    ''Now all the parameters are set etc, we just go through all the rows, 
    ''and all the columns and set the values, then execute the command, then change the values and execute again
    ''--> set the command up once and repeatedly execute it

    xlRow = 1   'only one row being used *as of now*, and that is the top row in the excel sheet
    xlCol = 119 'First column of misc. data
    While Cells(xlRow, xlCol) <> ""

       For a = 1 To Fields.Count - 1
           InsertQuery.Parameters("@p" & a).Value = Cells(xlRow, xlCol + a)
       Next a

       InsertQuery.Execute , , adExecuteNoRecords ''dont return a resultset
    Wend


    DBconnection.Close
    Set DBconnection = Nothing

    ErrorHandler:
    If Err.Number <> 0 Then
       Msg = "Error # " & Str(Err.Number) & " was generated by " _
       & Err.Source & Chr(13) & "Error Line: " & Erl & Chr(13) & Err.Description
       MsgBox Msg, , "Error", Err.HelpFile, Err.HelpContext
    End If
End Sub

You have 100 columns and 1000 rows to insert from an excel sheet:

  • You set up the command, INSERT INTO ... VALUES (... 100 @parameter placeholders...)
  • You clear the parameters collection (in case VBA has decided to 'help' by creating them) and add a load of parameters that represent your strings in your sheet
  • You then iterate the sheet, row by row, setting each column value on the row, into the relevant parameter and after you set all the columns (100 times), you execute the command then move to the next row, set the values again and execute again (1000 times)

I've got absolutely no way of testing this, sorry - it's my best guess but I fully expect it still has some error because I don't really know where you got Fields from. The answer with 8 votes from here was helpful: VBA, ADO.Connection and query parameters - I distinctly recall from when I was using VB6 about 20 years ago, that ADODB would try and prepopulate the parameters collection in certain circumstances, with its guesses at the parameter types; we routinely cleared it and added our own, but you might have some success proceeding with the default parameters it makes

The names of the parameters are not relevant; only the position. There's no requirement that @p1 from the query string matches the @p1 name given for the parameter - if the first parameter in the string were called @bob and you then cleared and added a parameter named @alice, whatever @alice's value was would be assigned to @bob because @bob is first in the query and @alice is first in the parameters collection. I used @pXXX as a parameter name for ease of reference in both cases

Caius Jard
  • 72,509
  • 5
  • 49
  • 80
  • This is where I got the DB.TableDefs(Table).Fields.Count property: https://stackoverflow.com/questions/19452952/how-to-count-number-of-fields-in-a-table I'm trying to find a way to count all of the columns in the table. – Noah Thomas Nov 19 '19 at 19:27
  • I tried the following statement: TblDef = NK21Data.TableDefs(Tbl).Fields.Count to find the number of columns and return as an integer but apparently "Object required" is the response. – Noah Thomas Nov 19 '19 at 19:49
  • Is the "table" the source excel sheet or the destination db table? – Caius Jard Nov 19 '19 at 19:49
  • Delete the nk21 statement, it seems unnecessary. Replace fields count with the number of columns from the excel – Caius Jard Nov 19 '19 at 19:50
  • It is the destination table in the SQL Database – Noah Thomas Nov 19 '19 at 19:50
1

Here is my basic ADODB Execute template. This isn't meant to be an answer but more a helpful post. It should assist in showing you what you're doing incorrectly, which appears to be simple syntax issues as well as being really new to this (formatting and other pieces of code suggest that maybe you've gotten yourself "googled into a corner.").

Private Sub ADODBExample()
    Dim vbSql As String, cnnstr as string
    Dim cnn As ADODB.Connection

    vbSql = "sql statement ;"
    Set cnn = New Connection
    cnnstr = "Provider=SQLOLEDB;Data Source=SERVERNAME;Initial Catalog=DBNAME;User ID=USERID;Password=PASSWORD; Trusted_Connection=No"
    cnn.Open cnnstr
    cnn.Execute vbSql
    cnn.Close
    Set cnn = Nothing
End Sub

More helpful tips -

  1. Stop looping through cells, ranges and other worksheet/book objects. Learn to use arrays - itll make processing way better.

  2. Simplicity is best. You appear to doing what I consider alot of unnecessary things, but then again I dont know all the requirements.

Doug Coats
  • 6,255
  • 9
  • 27
  • 49
0

So I amended the code to the following:

Sub Connection()

    Dim i As Integer, sqlIns As String, sqlVals As String
    Dim InsertQuery As New ADODB.Command
    Dim firstRow As Long, firstCol As Integer, lastCol As Integer, currRow As Integer
    Dim DBconnection As New ADODB.Connection
    Dim ConnString As String
    Dim Tbl As String

    ConnString = "Provider=sqloledb;Server=SERVER;Inital Catalog=DB;Integrated Security=SSPI;User ID=ID;Password=PW;"

    DBconnection.Open ConnString

    InsertQuery.ActiveConnection = DBconnection
    InsertQuery.CommandType = adCmdText

    If LH = True Then
        Tbl = "Info.CaseBLH"
        sqlIns = "INSERT INTO Info.CaseBLH("
    ElseIf RH = True Then
        Tbl = "Info.CaseBRH"
        sqlIns = "INSERT INTO Info.CaseBRH("
    Else
        MsgBox ("No available sheets")
        'Application.Quit
    End If

    ''build the command text side by side, named columns and values with param placeholders

    sqlVals = " VALUES("

    ''i could work these out by scanning the sheet i suppose. hardcoded for now
    firstRow = 1
    firstCol = 119
    lastCol = 231

    ''generate the SQL - its this that lets the column names come in any order in the sheet
    For i = firstCol To lastCol
        sqlIns = sqlIns & Cells(firstRow, i) & ","
        sqlVals = sqlVals & "?,"
        InsertQuery.Parameters.Append InsertQuery.CreateParameter("p" & i - firstCol, adVarChar, adParamInput, 255)
    Next i

    ''chop off the extra trailing commas and form a syntax correct command
    InsertQuery.CommandText = Left$(sqlIns, Len(sqlIns) - 1) & ")" & Left$(sqlVals, Len(sqlVals) - 1) & ")"

    ''iterate the data part of the sheet and execute the query repeatedlty
    currRow = firstRow ' - not needed as the data is automatically replaced with the code above
    While Cells(currRow, firstCol) <> ""

       For i = firstCol To lastCol - 1
           InsertQuery.Parameters("p" & i - firstCol).Value = Cells(currRow, i)
       Next i

       InsertQuery.Execute , , adExecuteNoRecords ''dont return a resultset

    Wend


    DBconnection.Close
    Set DBconnection = Nothing

ErrorHandler:
    If Err.Number <> 0 Then
       MsgBox Err.Description
    End If
End Sub

Right at the

InsertQuery.Execute , , adExecuteNoRecords

Line I'm getting a error telling me there is a syntax error around the ':' which doesn't make any sense to me. If I append my code to send the error to the error handler, every single row it cycles through throws me an error saying there is a syntax error around '-' or '/'. I think it has something to do with the parameter.value line.

Noah Thomas
  • 71
  • 1
  • 9
  • Do you get the column names out of the sheet? In my sheet the column names are exactly the same as the names in the table. Because the sql is built from the sheet the sheet must have syntactically valid column name headers. You can't have a sheet header say "name: first", "name - last" etc. Show a screenshot of your table's column names (viewed in Access) and another screenshot of all your sheet column header texts. Or, put a breakpoint on the while line, and write `?insertquery.commandtext` into the immediate window and copy paste the result into you posting. I bet it's a sql syntax issue – Caius Jard Nov 20 '19 at 14:31
  • @CaiusJard I see what you are saying, I believe. The sheet you created has the same column names as the SQL table. That explains another problem: the first cycle through the program left all NULL characters. My Excel Sheet does not have the column names above the data. – Noah Thomas Nov 20 '19 at 15:30
  • You'll probably either have to form your sql without the column names then and make sure you put the same count of parameters as there are columns in the table or you'll have to pre program your sql in the cvs with all column names of the table. If the sheet data columns are in a different order to the table columns then it will be mandatory to list all the table columns in the sql, in the same order as the sheet data is – Caius Jard Nov 20 '19 at 17:12
  • For example, if your sheet is in the same order as the table and they both have the same 3 columns of "name, age, weight" then your sql string can be simply `insert into tableX values (?,?,?)` when it's finished being built. If the sheet data is "weight,name,age" and the table is "name,weight,height,age" (different order, different count) your sql will have to name the columns in sheet order and be a sql if `insert into tableX(weight,Name.age) values(?,?,?)` - the height column in the table will be null (no sheet data, not mentioned in sql) – Caius Jard Nov 20 '19 at 17:16