In this tip, I am using MFC CDatabase and CRecordset to deal with databases through ODBC. I noticed that there is a limit of 255 columns in MFC library, and I wanted to go through this damned limit.
Using the Code
The native CRecordset
is limited with 255 columns. See code from dbcore.cpp.
UINT CRecordset::BindFieldsToColumns()
{
ASSERT_VALID(this);
ASSERT(m_hstmt != SQL_NULL_HSTMT);
ASSERT(m_nFieldsBound == 0);
ASSERT(m_nFields != 0 && m_nFields <= 255);
...
}
m_nFields
is short, which means, it should handle more than 255.
Answer by Microsoft:
The reason is historical, I think. Prior to SQL 7, the 255 column limit was one that a lot of databases had, and the MFC developers had to pick some limit, and I guess that was the one that they chose. One workaround may be to specify fields rather than doing a Select *, and get the first 200 fields in one recordset, the next 200 fields in the second, etc. Be aware that if you do this, there is a limit to how large or complex a select can be, and you may run into it.. Another workaround would be to use ODBC API, which doesn't have this limitation. You may need to add memory though. ODBC is limited only by the system memory, but it's going to take a LOT of memory to load 100000 rows of 1500 columns. I hope this helps, Russ Gray Microsoft Developer Support
Reading the CRecordset
definition in ...\MFC\include\AFXDB.H, I noticed that BindFieldsToColumns()
is called at one point. The functions are virtual, so if you derive the CRecordset
class to another one, you can easily bypass the 255 limit.
First, override the two functions Move(..)
and SetRowsetSize(..)
and just change calls to InitRecord()
and BindFieldsToColumns()
to your own functions (for example, _InitRecord(..)
and _BindFieldsToColumns(..)
)
myRecordSet::myRecordSet(...): CRecordset(pdb)
void myRecordSet::Move(long nRows, WORD wFetchType)
{
ASSERT_VALID(this);
ASSERT(m_hstmt != SQL_NULL_HSTMT);
if (m_nFieldsBound == 0)
{
_InitRecord(); ResetCursor();
}
if (m_nFieldsBound > 0)
{
memset(m_pbFieldFlags, 0, m_nFields);
m_nEditMode = noMode;
}
CheckRowsetCurrencyStatus(wFetchType, nRows);
RETCODE nRetCode;
if ((wFetchType == SQL_FETCH_FIRST ||
wFetchType == SQL_FETCH_LAST ||
wFetchType == SQL_FETCH_NEXT ||
wFetchType == SQL_FETCH_PRIOR ||
wFetchType == SQL_FETCH_RELATIVE) &&
m_dwOptions & skipDeletedRecords)
{
SkipDeletedRecords(wFetchType, nRows, &m_dwRowsFetched, &nRetCode);
}
else
nRetCode = FetchData(wFetchType, nRows, &m_dwRowsFetched);
SetRowsetCurrencyStatus(nRetCode, wFetchType, nRows, m_dwRowsFetched);
if (m_nFields > 0 && !IsEOF() && !IsBOF() &&
!(m_dwOptions & useMultiRowFetch))
{
Fixups();
}
}
void myRecordSet::SetRowsetSize(DWORD dwNewRowsetSize)
{
ASSERT_VALID(this);
ASSERT(dwNewRowsetSize > 0);
if (!IsOpen())
{
m_dwRowsetSize = dwNewRowsetSize;
return;
}
if (!(m_dwOptions & useMultiRowFetch))
{
ASSERT(FALSE);
return;
}
if (m_dwAllocatedRowsetSize == 0 ||
(m_dwAllocatedRowsetSize < dwNewRowsetSize))
{
FreeRowset();
m_rgRowStatus = new WORD[dwNewRowsetSize];
if (!(m_dwOptions & userAllocMultiRowBuffers))
{
m_dwRowsetSize = dwNewRowsetSize;
CFieldExchange fx(CFieldExchange::AllocMultiRowBuffer, this);
DoBulkFieldExchange(&fx);
m_dwAllocatedRowsetSize = dwNewRowsetSize;
int nOldFieldsBound = m_nFieldsBound;
m_nFieldsBound = 0;
_InitRecord(); m_nFieldsBound = nOldFieldsBound;
}
}
else
{
m_dwRowsetSize = dwNewRowsetSize;
}
RETCODE nRetCode;
AFX_SQL_SYNC(::SQLSetStmtOption(m_hstmt, SQL_ROWSET_SIZE, m_dwRowsetSize));
}
Nothing changes in this one except the number of maximum of columns set here to 1024 or it can be set in a global variable with a call to SQLGetInfo(...)
with SQL_MAX_COLUMNS_IN_TABL
E.
UINT myRecordSet::_BindFieldsToColumns() {
ASSERT_VALID(this);
ASSERT(m_hstmt != SQL_NULL_HSTMT);
ASSERT(m_nFieldsBound == 0);
ASSERT(m_nFields != 0 && m_nFields <= 1024);
CFieldExchange fx(CFieldExchange::BindFieldToColumn, this);
fx.m_hstmt = m_hstmt;
if (m_dwOptions & useMultiRowFetch)
DoBulkFieldExchange(&fx);
else
DoFieldExchange(&fx);
return fx.m_nFields;
}
And finally, change the call from BindFieldsToColumns()
to _BindFieldsToColumns()
. That's all. :)
void myRecordSet::_InitRecord() {
if (m_nFields != 0)
{
m_nFieldsBound = _BindFieldsToColumns(); ASSERT((int)m_nFields == m_nFieldsBound);
if (m_nFields > 0 && m_bCheckCacheForDirtyFields)
AllocDataCache();
}
else
m_nFieldsBound = -1;
}
Points of Interest
It took me fifteen minutes to do this. Four functions to copy, and only change three calls, instead of writing my own class from scratch in native ODBC API as Microsoft says.
History
- 21st May, 2021: First version & should be the only one