views:

337

answers:

2

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.

A: 

A blob is just binary data, so you'll need to use some form of byte array.

Randolph Potter
Sorry, but your answer really doesn't help me in the context of OLE DB.
Jon Bright
A: 

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.

Jon Bright