Search code examples
sql-serveroledbsql-server-native-client

Inserting a BLOB with OLE DB


I'm working on an app which is using OLE DB and SQL Server Native Client to access a SQL Server DB. To date, I've only been dealing with fairly simple SQL. For this, I've been obtaining an ICommandText and using SetCommandText. I now want to insert a large object into the database. I see that ICommandStream exists, but it seems like using this would require me to add a class that implements IStream and also to quote my BLOB appropriately (escaping apostrophes, etc.). Surely there's an easier way?

Side note: OLE DB wasn't my choice and I can't change it at this stage. So the easier way "use something higher-level" isn't available.


Solution

  • It turns out, there's an answer on the Microsoft SQLNCli team blog.

    To expand on this, here's the code I ended up using. First, you need an ISequentialStream for SQL Server Native Client to be reading from. I have my data in memory, so I could just construct this with a pointer to my BLOB, but it's trivial to go and get the data from elsewhere. It's not part of the contract, but it's perhaps useful to know that the reads seem to happen in 1024-byte chunks. Here's my stream class:

    struct ISequentialStream;
    
    class XYZSQLStream : public ISequentialStream
    {
    public:
        XYZSQLStream(LPBYTE data, __int64 ulLength);
        virtual ~XYZSQLStream();
    
        virtual BOOL Clear();
        virtual ULONG Length() { return m_cBufSize; };
    
        virtual operator void* const() { return m_pBuffer; };
    
        STDMETHODIMP_(ULONG) AddRef(void);
        STDMETHODIMP_(ULONG) Release(void);
        STDMETHODIMP QueryInterface(REFIID riid, LPVOID *ppv);
    
        STDMETHODIMP Read(void __RPC_FAR *pv, ULONG cb, ULONG __RPC_FAR *pcbRead);
        STDMETHODIMP Write(const void __RPC_FAR *pv, ULONG cb, ULONG __RPC_FAR *pcbWritten);
    
    private:
        ULONG m_cRef;   // reference count
        void* m_pBuffer;   // buffer
        ULONG m_cBufSize;   // buffer size
        ULONG m_iPos;   // current index position in the buffer
    };
    

    The implementation of this is trivial:

    XYZSQLStream::XYZSQLStream(LPBYTE data, ULONG ulLength)
    {
        m_iPos = 0;
        m_cRef = 0;
        m_pBuffer = data;
        m_cBufSize = ulLength;
    
        AddRef();
    }
    
    XYZSQLStream::~XYZSQLStream()
    {
        // Shouldn't have any references left
        if (m_cRef)
            throw L"Destroying SQLStream with references";
        delete[] m_pBuffer;
    }
    
    ULONG XYZSQLStream::AddRef()
    {
        return ++m_cRef;
    }
    
    ULONG XYZSQLStream::Release()
    {
        if (!m_cRef)
            throw L"Releasing referenceless SQLStream";
        if (--m_cRef)
            return m_cRef;
    
        delete this;
        return 0;
    }
    
    HRESULT XYZSQLStream::QueryInterface(REFIID riid, void** ppv)
    {
        if (!ppv)
            return E_INVALIDARG;
        *ppv = NULL;
    
        if (riid == IID_IUnknown)
            *ppv = this;
    
        if (riid == IID_ISequentialStream)
            *ppv = this;
    
        if(*ppv) 
        {
            ((IUnknown*)*ppv)->AddRef();
            return S_OK;
        }
    
        return E_NOINTERFACE;
    }
    
    BOOL XYZSQLStream::Clear()
    {
        m_iPos = 0;
        m_cBufSize = 0;
    
        m_pBuffer = NULL;
    
        return TRUE;
    }
    
    HRESULT XYZSQLStream::Read(void *pv, ULONG cb, ULONG* pcbRead)
    {
        if (pcbRead)
            *pcbRead = 0;
    
        if (!pv)
            return STG_E_INVALIDPOINTER;
    
        if (cb == 0)
            return S_OK;
    
        ULONG cBytesLeft = m_cBufSize - m_iPos;
        ULONG cBytesRead = cb > cBytesLeft ? cBytesLeft : cb;
    
        //DEBUG(L"cb %d, left %d, read %d\n", cb, cBytesLeft, cBytesRead);
    
        if (cBytesLeft == 0)
            return S_FALSE; 
    
        // Copy to users buffer the number of bytes requested or remaining
        memcpy(pv, (void*)((BYTE*)m_pBuffer + m_iPos), cBytesRead);
        m_iPos += cBytesRead;
    
        if (pcbRead)
            *pcbRead = cBytesRead;
    
        if (cb != cBytesRead)
            return S_FALSE; 
    
        return S_OK;
    }
    
    HRESULT XYZSQLStream::Write(const void *pv, ULONG cb, ULONG* pcbWritten)
    {
        // Parameter checking
        if (!pv)
            return STG_E_INVALIDPOINTER;
    
        if (pcbWritten)
            *pcbWritten = 0;
    
        if (cb == 0)
            return S_OK;
    
        // Enlarge the current buffer
        m_cBufSize += cb;
    
        // Need to append to the end of the stream
        m_pBuffer = CoTaskMemRealloc(m_pBuffer, m_cBufSize);
        memcpy((void*)((BYTE*)m_pBuffer + m_iPos), pv, cb);
        // m_iPos += cb;
    
        if (pcbWritten)
            *pcbWritten = cb;
    
        return S_OK;
    }
    

    Using an ICommandText, you can then execute a SELECT on the table. You're not actually going to retrieve any data using this, it's just a way of getting an IRowsetChange. I have an extra ExecuteCommand method for this. The SQL passed in pSQL is (similar to) SELECT x,y,z FROM TableWithBlob. FAIL is a custom macro that records the problem and returns.

    HRESULT XYZSQLCommand::ExecuteCommand(TCHAR* pSQL, IRowset** ppRowSet, IRowsetChange** ppRowSetChange)
    {
        HRESULT hr;
        IRowsetChange* pIRowsetChange;
        IRowset* pIRowset;
        hr = m_pICommandText->SetCommandText(DBGUID_DBSQL, pSQL);
        if (FAILED(hr))
            FAIL(hr);
    
        hr = m_pICommandText->Execute(NULL, IID_IRowsetChange, NULL, NULL, (IUnknown**)&pIRowsetChange);
        if (FAILED(hr))
            FAIL(hr);
    
        hr = pIRowsetChange->QueryInterface(IID_IRowset, (void**)&pIRowset);
        if (FAILED(hr))
        {
            pIRowsetChange->Release();
            FAIL(hr);
        }
    
        *ppRowSet = pIRowset;
        *ppRowSetChange = pIRowsetChange;
        return S_OK;
    }
    

    I now have an IRowset and an IRowsetChange for the table in question. You then construct a DBBINDING as you normally would. I'm eliding this - it's not really relevant to the question. The relevant bit is:

    static DBOBJECT streamObj = {STGM_READ, IID_ISequentialStream};
    pDBBindings[nCol].pObject = &streamObj;
    pDBBindings[nCol].wType = DBTYPE_IUNKNOWN;
    pDBBindings[nCol].cbMaxLen = sizeof(ISequentialStream*);
    

    When subsequently filling in the matching data memory block, you can then do this (sorry for the ugly casts):

    XYZSQLStream *stream = new XYZSQLStream(data_to_write, length_of_data);
    *((ISequentialStream**)(pbData+pDBBindings[x].obValue)) = stream;
    *((DBLENGTH*)(pbData+pDBBindings[x].obLength)) = (DBLENGTH)length_of_data;
    *((DBSTATUS*)(pbData+pDBBindings[x].obStatus)) = DBSTATUS_S_OK;
    

    Get yourself an IAccessor using your IRowsetChange and bind it:

    IAccessor* pIAccessor;
    HACCESSOR hAccessor;
    DBBINDSTATUS* pDBBindStatus;
    
    hr = pRowsetChange->QueryInterface(IID_IAccessor, (void**) &pIAccessor);
    // Error handling elided
    
    pDBBindStatus = new DBBINDSTATUS[ulCols];
    
    //Associate the bindings with the data accessor for the rowset
    hr = pIAccessor->CreateAccessor( DBACCESSOR_ROWDATA, ulCols, pDBBindings, 0, hAccessor, pDBBindStatus);
    // Error handling, cleanup elided
    

    Finally, you can insert your row:

    hr = pRowsetChange->InsertRow(NULL, hAccessor, pbData, NULL);
    

    SQL Server Native Client will read from your stream and insert the row. The hoop-jumping is now done. ReleaseAccessor, cleanup, etc. elided.