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.