Attributed ATL: Accessing BLOB with ISequentialStream

Before attributed ATL was deprecated, it was a convenient way to access databases using attributed classes on top of OLEDB Consumer Templates. Does not it look nice?

[
    db_command("SELECT ServerData FROM Server WHERE Server = ?")
]
class CGetServerData
{
public:
    [ db_param(1) ] LONG m_nServer;
    [ db_column(1, length = "m_nDataLength") ] ISequentialStream* m_pDataStream; DBLENGTH m_nDataLength;
};

It worked great with Visual Studio .NET 2003 and it failed to work with later releases. There are questions on internet about the problem, but there few answers if any. As I recently had to convert a project from 2003 version of the compiler to Visual Studio 2008, the problem was finally to be resolved.

The first problem, and an easier one was that you have to put attribute values in quotes. ‘Db_column(1, …’ has to be “db_column(“1”, …’ and besides going through code and making changes, it appeared that if earlier implementation managed to distinguish between ordinal column numbers and field names by checking provided argument to be a number or a string, with Visual Studio 2008 one has to provide strings at any time. If the string is a valid integer, the attribute processor will treat it as an ordinal number.

So if you want your code to be both compiled with 2003 and 2008 versions of Visual Studio, you have a puzzle now: you have to use quotes, but version 2003 will no longer treat the argument as ordinal number. Note that version 2003 will keep compiling the code and the problem will only come up on runtime. As eventually in the project of interest support for version 2003 will be dropped, I was not interested in this problem anymore.

The main problem is related to use of ISequentialStream pointer variable as a data field. Again, it worked great with Visual Studio .NET 2003, it no longer did with Visual Studio .NET 2005. First of all, you cannot use this data type anymore, as templates don’t provide an automatic mapper to OLE DB Type.� Instead of ISequentialStream you have to use IUnknown and QueryInterface the provided pointer before use.

This solves compilation problem, but still the code does not work on runtime. An attempt to bind to the column using such accessor results in DB_E_ERRORSOCCURRED (0x80040E21) “Multiple-step OLE DB operation generated errors. Check each OLE DB status value, if available. No work was done.” A more thorough look gave DBBINDSTATUS_BADBINDINFO (3) for binding to the column of interest.

The cause of the problem is missing DBOBJECT object pointer in the corresponding DBBINDING::pObject field. Why? A general rule for troubleshooting this kind of problem is to enable expanding attributed source code to check what exactly is generated.

The problem is that unlike previous versions of Visual Studio, ATL attributed code provider is no longer generates correct OLEDB template map code for BLOB fields. And this results in incorrectly generated DBBINDING structure, inability to bind to column and as a final result in DB_E_ERRORSOCCURRED error code which does not give a sufficient clue on the root of the problem.

While the map needs a BLOB_* macro for the BLOB column (e.g. BLOB_ENTRY or BLOB_ENTRY_LENGTH), the generated code uses _COLUMN_ENTRY_CODE instead. This causes missing (null) DBBINDING::pObject field in the binding structure:

BEGIN_ACCESSOR_MAP(_CGetServerDataAccessor, 1)
    BEGIN_ACCESSOR(0, true)
        //_COLUMN_ENTRY_CODE(1, DBTYPE_IUNKNOWN, _SIZE_TYPE(m_pDataUnknown), 0, 0, offsetbuf(m_pDataUnknown), offsetbuf(m_nDataLength), 0)
        BLOB_ENTRY_LENGTH(1, __uuidof(ISequentialStream), STGM_READ, m_pDataUnknown, m_nDataLength)
    END_ACCESSOR()
END_ACCESSOR_MAP()

So it appears that to fix the problem, an attributed class needs to be replaced with a non-attributed update. To ease the conversion one can expand attributed source and use it as a base for the non-attributed class.

And example of such correction/replacement is provided below for a reference.

#if TRUE
class _CGetServerDataAccessor
{
public:

DEFINE_COMMAND_EX(_CGetServerDataAccessor, L"SELECT Data FROM Server WHERE Server = ?")

BEGIN_PARAM_MAP(_CGetServerDataAccessor)
    SET_PARAM_TYPE(DBPARAMIO_INPUT)
    //_COLUMN_ENTRY_CODE(1, _OLEDB_TYPE(m_nServer), _SIZE_TYPE(m_nServer), 0, 0, offsetbuf(m_nServer), 0, 0)
    COLUMN_ENTRY(1, m_nServer)
END_PARAM_MAP()

BEGIN_ACCESSOR_MAP(_CGetServerDataAccessor, 1)
    BEGIN_ACCESSOR(0, true)
        //_COLUMN_ENTRY_CODE(1, DBTYPE_IUNKNOWN, _SIZE_TYPE(m_pDataUnknown), 0, 0, offsetbuf(m_pDataUnknown), offsetbuf(m_nDataLength), 0)
        BLOB_ENTRY_LENGTH(1, __uuidof(ISequentialStream), STGM_READ, m_pDataUnknown, m_nDataLength)
    END_ACCESSOR()
END_ACCESSOR_MAP()

public:
    LONG m_nServer;
    IUnknown* m_pDataUnknown;
    DBLENGTH m_nDataLength;
};

class CGetServerData :
    public CCommand<CAccessor<_CGetServerDataAccessor> >
{
public:
// CGetServerData
    HRESULT OpenRowset(const CSession& Session, LPCWSTR pszCommand = NULL) throw()
    {
        if(!pszCommand)
            _V(_CGetServerDataAccessor::GetDefaultCommand(&pszCommand));
        return Open(Session, pszCommand, NULL);
    }
};
#else
[
    db_command("SELECT Data FROM Server WHERE Server = ?")
]
class CGetServerData
{
public:
    [ db_param("1") ] LONG m_nServer;
    [ db_column("1", length = "m_nDataLength") ] IUnknown* m_pDataUnknown; DBLENGTH m_nDataLength;
};
#endif

Leave a Reply