SYMPTOMS
When using the ATL OLE DB Consumer Templates and specifying DBTYPE_BYREF for the data type for a column entry,
MoveNext returns DB_E_BADBINDINFO. More specifically, internally
IRowset::GetData is called and it returns the error. The column map may resemble the following:
BEGIN_COLUMN_MAP(CMyTableAccessor)
COLUMN_ENTRY(1, m_field1)
COLUMN_ENTRY_EX(2, DBTYPE_STR | DBTYPE_BYREF, sizeof(char *),
0, 0, m_pszField2, m_nField2Len, m_Field2Status)
END_COLUMN_MAP()
CAUSE
The OLE DB provider is doing deferred accessor validation and is finding values of the DBBINDING structure, which it doesn't support. Specifically, Active Template Library (ATL) is setting the dwMemOwner member of the DBBINDING structure to DBMEMOWNER_PROVIDEROWNED whenever the developer specifies DBTYPE_BYREF. This is done so that the consumer doesn't have the responsibility to free the memory returned by the provider. When
ReleaseRows is called in the
MoveNext method, the memory is automatically freed by the provider. However, according to the OLE DB specification:
For bindings in row accessors, consumer-owned memory must be used unless
wType is DBTYPE_BSTR, X | DBTYPE_BYREF, X | DBTYPE_ARRAY, or X |
DBTYPE_VECTOR, in which cases either consumer-owned or provider-owned
memory can be used. However, the provider might not support using
provider-owned memory to retrieve columns for which
IColumnsInfo::GetColumnInfo returns DBCOLUMNFLAGS_ISLONG for the column.
ODBC OLE DB Provider is one such provider that doesn't support DBMEMOWNER_PROVIDEROWNED on DBCOLUMNFLAGS_ISLONG columns. As a result, the error is returned when a SQL Server column type of "text" is used, for example, and DBTYPE_BYREF is specified (which causes ATL to set dwMemOwner to DBMEMOWNER_PROVIDEROWNED).
RESOLUTION
The type DBTYPE_BYREF can still be used, but modifications must be made to the ATL code to prevent it from setting the the DBBINDING structure to DBMEMOWNER_PROVIDEROWNED. Furthermore, if you do specify DBMEMOWNER_CLIENTOWNED and use DBTYPE_BYREF, the consumer is responsible for freeing the memory referenced by using
IMalloc::Free/
CoTaskMemFree.
The easiest way to work around this problem is to copy the Atldbcli.h file into your project so that you have your own custom build of the header. Modify the
Bind method of
CAccessorBase by commenting the code that sets the dwMemOwner member to DBMEMOWNER_PROVIDEROWNED. For example:
static void Bind(DBBINDING* pBinding, ULONG nOrdinal, DBTYPE wType,
ULONG nLength, BYTE nPrecision, BYTE nScale, DBPARAMIO eParamIO,
ULONG nDataOffset, ULONG nLengthOffset = NULL, ULONG nStatusOffset = NULL,
DBOBJECT* pdbobject = NULL)
{
ATLASSERT(pBinding != NULL);
// If we are getting a pointer to the data, then let the provider
// own the memory.
// if (wType & DBTYPE_BYREF)
// pBinding->dwMemOwner = DBMEMOWNER_PROVIDEROWNED;
// else
pBinding->dwMemOwner = DBMEMOWNER_CLIENTOWNED;
Use this header file instead of the Visual C++ copy.
NOTE: You must free the memory returned to you by the provider. You may want to create a function called FreeRecordMemory() in your class where you call
CoTaskMemFree on any of the pointers you get back with BYREF. For example:
void FreeRecordMemory()
{
CoTaskMemFree((BYTE *)m_pszField2);
}
If you don't want to modify the header file, there is a more difficult approach to work around this problem. You can write your own macro that calls your own
Bind function, and that always sets pBinding->dwMemOwner = DBMEMOWNER_CLIENTOWNED.
The code for your data class might resemble the following:
#define _COLUMN_ENTRY_CODE_BYREF(nOrdinal, wType, nLength, nPrecision, nScale, dataOffset, lengthOffset, statusOffset) \
if (pBuffer != NULL) \
{ \
CAccessorBase::FreeType(wType, pBuffer + dataOffset); \
} \
else if (pBinding != NULL) \
{ \
Bind(pBinding, nOrdinal, wType, nLength, nPrecision, nScale, eParamIO, \
dataOffset, lengthOffset, statusOffset); \
pBinding++; \
} \
nColumns++;
#define COLUMN_ENTRY_EX_BYREF(nOrdinal, wType, nLength, nPrecision, nScale, data, length, status) \
_COLUMN_ENTRY_CODE_BYREF(nOrdinal, wType, nLength, nPrecision, nScale, offsetbuf(data), offsetbuf(length), offsetbuf(status))
class CdbotexttestAccessor
{
public:
TCHAR m_szfield1[11];
char * m_pszField2;
long m_nField2Len;
DBSTATUS m_Field2Status;
static void Bind(DBBINDING* pBinding, ULONG nOrdinal, DBTYPE wType,
ULONG nLength, BYTE nPrecision, BYTE nScale, DBPARAMIO eParamIO,
ULONG nDataOffset, ULONG nLengthOffset = NULL, ULONG nStatusOffset = NULL,
DBOBJECT* pdbobject = NULL)
{
ATLASSERT(pBinding != NULL);
// If we are getting a pointer to the data, then let the provider
// own the memory.
// (wType & DBTYPE_BYREF)
// pBinding->dwMemOwner = DBMEMOWNER_PROVIDEROWNED;
// else
pBinding->dwMemOwner = DBMEMOWNER_CLIENTOWNED;
pBinding->pObject = pdbobject;
pBinding->eParamIO = eParamIO;
pBinding->iOrdinal = nOrdinal;
pBinding->wType = wType;
pBinding->bPrecision = nPrecision;
pBinding->bScale = nScale;
pBinding->dwFlags = 0;
pBinding->obValue = nDataOffset;
pBinding->obLength = 0;
pBinding->obStatus = 0;
pBinding->pTypeInfo = NULL;
pBinding->pBindExt = NULL;
pBinding->cbMaxLen = nLength;
pBinding->dwPart = DBPART_VALUE;
if (nLengthOffset != NULL)
{
pBinding->dwPart |= DBPART_LENGTH;
pBinding->obLength = nLengthOffset;
}
if (nStatusOffset != NULL)
{
pBinding->dwPart |= DBPART_STATUS;
pBinding->obStatus = nStatusOffset;
}
}
void FreeRecordMemory()
{
CoTaskMemFree((BYTE *)m_pField2);
}
BEGIN_COLUMN_MAP(CdbotexttestAccessor)
COLUMN_ENTRY(1, m_szField1)
COLUMN_ENTRY_EX_BYREF(2, DBTYPE_STR | DBTYPE_BYREF, sizeof(char *), 0, 0, m_pszField2, m_szField2Len, m_szField2Status)
END_COLUMN_MAP()
};
You still can use BYREF, however, you must specify client owned by modifying the ATL code. You are also responsible for freeing the memory using
IMalloc::Free/
CoTaskMemFree.