2

I need to upload a file (<10 MB) around once a week to a SQL Server 2016 database on a remote server in the same network. Until now it was all within a Access FE/BE but I want to migrate to SQL Server as backend.

The attachments I had in MS Access so need to be handled now on the SQL database as I do not want to do this on a fileshare.

I found many threads about using something like this from SQLShack

DECLARE @File varbinary(MAX);  

SELECT  
    @File = CAST(bulkcolumn AS varbinary(max))  
FROM  
    OPENROWSET(BULK 'C:\sqlshack\akshita.png', SINGLE_BLOB) as MyData; 
 
INSERT INTO DemoFileStreamTable_1  
VALUES  (NEWID(), 'Sample Picture', @File)

This works when I start the query within SSMS on the SQL Server itself and the file is already accessible by the server on its local drive.

But when I try to put this in my VBA code on my Access frontend computer:

Sub DaoOdbcExample()
    Dim cdb As DAO.Database, qdf As DAO.QueryDef
    Set cdb = CurrentDb
    Set qdf = cdb.CreateQueryDef("")
    qdf.Connect = "ODBC;" & _
            "Driver={SQL Server};" & _
            "Server=MyServer;" & _
            "Database=MyDatabase;" & _
            "Trusted_Connection=yes;"
    qdf.SQL = "DECLARE @File varbinary(MAX); SELECT @File = CAST(bulkcolumn as varbinary(max))  FROM  OPENROWSET(BULK 'D:\SomeFile.pdf', SINGLE_BLOB) as MyData; INSERT INTO DemoFileStreamTable_1  VALUES  (  NEWID(),  'Test PDF',  @File)"
    qdf.ReturnsRecords = False
    qdf.Execute dbFailOnError
    Set qdf = Nothing
    Set cdb = Nothing
End Sub

I just get an error

ODBC--call failed

Other simple "Select" statements seem to work, so the connection itself seems okay.

So my questions are:

  1. How can I perform such an upload from a local file on computer A to the remote SQL server on computer B (which cannot directly access this file) using MS Access as my frontend?

  2. Is there a different way not using the "BULK" statement as I need "bulkadmin" rights for all users then?

marc_s
  • 732,580
  • 175
  • 1,330
  • 1,459
karlo922
  • 141
  • 8
  • Aside... `CAST(bulkcolumn as varbinary(max))` is redundantly redundant because [`openrowset(... single_blob)`](https://learn.microsoft.com/en-us/sql/t-sql/functions/openrowset-transact-sql#single_blob) already returns `bulk_column` as `varbinary(max)`. – AlwaysLearning Mar 12 '22 at 09:24
  • Is the SQL Server instance installed on the same machine as the machine executing the VBA script? If not - it's not going to work. This is because the query is executing on the SQL Server instance itself, so the `D:\SomeFile.pdf` filepath is relative to the SQL Server instance. If that path exists on a remote client computer then SQL Server cannot find it and so cannot read it in. If you have SQL Server and VBA running on separate computers the only way you'll be able to make this work (with openrowset) is by a Shared Folder that's accessible to both parties. – AlwaysLearning Mar 12 '22 at 09:27
  • 1
    Rather than trying to use openrowset via a shared folder you might find it easier to binary read the file into a byte array (or stream) in VBA and then post it to SQL Server as a varbinary(max) parameter via a DAO.Recordset. This [previous SO question/answer](https://stackoverflow.com/a/49522352/390122) should be a starting point, but I haven't tested it myself. – AlwaysLearning Mar 12 '22 at 09:40
  • @AlwaysLearning: The link seems very promising but how would I connect to my remote DB via the ADOB then? – karlo922 Mar 12 '22 at 10:03
  • I got the download working using the link you provided. And yes, the files are not on the same machine so you are right that the openrowset approach will not work. But I'm still struggeling with the upload. – karlo922 Mar 12 '22 at 10:44

1 Answers1

3

I may have found a solution using the links from @AlwaysLearning. The first sub actually answers my question to upload a file to a remote FILESTREAM SQL Server. The second sub downloads all uploaded files into a given directory.

Private Sub btn_AddAtachment_Click()
    Dim cn, rs  As Object
    Dim sql, strCnxn, FileToUpload, FileName As String

    'FileSystemObject to do so some file checks
    Dim fso As Object
    Set fso = VBA.CreateObject("Scripting.FileSystemObject")
    
    'select file to upload, will open a FileOpenDialog
    FileToUpload = CustOpenFileDialog
    If FileToUpload <> "" Then
        FileName = fso.GetFileName(FileToUpload) 'get only filename + extension
         
        'SQL Connection
        strCnxn = "Provider=sqloledb;" & _
        "Data Source=MYSERVER;" & _
        "Initial Catalog=MYDATABASE;" & _
        "Integrated Security=SSPI;" 'Windows-Authentication
         
        Set cn = CreateObject("ADODB.Connection")
        cn.Open strCnxn
         
        'Recordset
        sql = "DemoFileStreamTable_1" 'Table to add file
        Set rs = CreateObject("ADODB.Recordset")
        rs.Open sql, strCnxn, 1, 3  '1 - adOpenKeyset, 3 - adLockOptimistic"
         
        'Create Stream to upload File as BLOB data
        Dim strm As Object
        Set strm = CreateObject("ADODB.Stream")
        strm.Type = 1 '1 - adTypeBinary
        strm.Open
        strm.LoadFromFile FileToUpload
        
        'Insert into database
        rs.AddNew 'FileId will be automatically handled by SQL
        rs!File = strm.Read
        rs!FileName = FileName
        strm.Close
        rs.Update
    End If
End Sub

Private Sub btn_DwnldSQL_Click()
    Dim cn, rs  As Object
    Dim sql As String
    Dim oStream As Object
    Dim OutputPath, strCnxn, FileName, SaveLocation As String
    
    OutputPath = "D:\ExportTest"
    
    'FileSystemObject to do so some file checks
    Dim fso As Object
    Set fso = VBA.CreateObject("Scripting.FileSystemObject")
    
    'SQL Connection
    Set cn = CreateObject("ADODB.Connection")
    strCnxn = "Provider=sqloledb;" & _
    "Data Source=MYSERVER;" & _
    "Initial Catalog=MYDATABASE;" & _
    "Integrated Security=SSPI;" 'Windows-Authentication
    
    cn.Open strCnxn 
    
    'your sql statment including varbinary max field here it is File
    sql = " SELECT [File],[FileName] from [DemoFileStreamTable_1] "
    
    'Recordset
    Set rs = CreateObject("ADODB.Recordset")
    rs.Open sql, cn
    
    'Actual Download
    Do Until rs.EOF 'Read all rows
        Set oStream = CreateObject("ADODB.Stream")
        FileName = CStr(rs.Fields("FileName").Value) 'FileName from Database field
        SaveLocation = fso.BuildPath(OutputPath, FileName) 'Create outputpath
        With oStream
             .Type = 1 '1 - adTypeBinary
             .Open
             .Write rs.Fields("File").Value 'actual BLOB data
             .SaveToFile SaveLocation, 2 '2 - adSaveCreateOverWrite
             .Close
        End With
        Set oStream = Nothing
        rs.MoveNext
    Loop
    rs.Close
    cn.Close
End Sub

Function CustOpenFileDialog() As String 
    Const msoFileDialogFilePicker As Long = 3
    Dim objDialog As Object
    Set objDialog = Application.FileDialog(msoFileDialogFilePicker)
    Dim fso As Object
    Set fso = VBA.CreateObject("Scripting.FileSystemObject")
    Dim FileName As String

    With objDialog
        .AllowMultiSelect = False
        ' Set the title of the dialog box.
        .Title = "Please select one file"
        ' Clear out the current filters, and add our own.
        .Filters.Clear
        .Filters.Add "supported Types", "*.pdf, *.xml, *.gltf, *.jpg, *.png"
 
        ' Show the dialog box. If the .Show method returns True, the
        ' user picked at least one file. If the .Show method returns
        ' False, the user clicked Cancel. 
        If .Show = True Then
            CustOpenFileDialog = .SelectedItems(1)
        Else
            CustOpenFileDialog = ""
        End If
    End With
End Function

Dharman
  • 30,962
  • 25
  • 85
  • 135
karlo922
  • 141
  • 8