home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Tricks of the Windows Gam…ming Gurus (2nd Edition)
/
Disc2.iso
/
vc98
/
mfc
/
src
/
dbcore.cpp
< prev
next >
Wrap
C/C++ Source or Header
|
1998-06-16
|
115KB
|
4,683 lines
// This is a part of the Microsoft Foundation Classes C++ library.
// Copyright (C) 1992-1998 Microsoft Corporation
// All rights reserved.
//
// This source code is only intended as a supplement to the
// Microsoft Foundation Classes Reference and related
// electronic documentation provided with the library.
// See these sources for detailed information regarding the
// Microsoft Foundation Classes product.
#include "stdafx.h"
#ifdef AFX_DB_SEG
#pragma code_seg(AFX_DB_SEG)
#endif
#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
#define new DEBUG_NEW
/////////////////////////////////////////////////////////////////////////////
// Global data
#ifdef _DEBUG
BOOL bTraceSql = FALSE;
#endif
AFX_STATIC_DATA const TCHAR _afxODBCTrail[] = _T("ODBC;");
AFX_STATIC_DATA const TCHAR _afxComma[] = _T(",");
AFX_STATIC_DATA const TCHAR _afxLiteralSeparator = '\'';
AFX_STATIC_DATA const TCHAR _afxCall[] = _T("{CALL ");
AFX_STATIC_DATA const TCHAR _afxParamCall[] = _T("{?");
AFX_STATIC_DATA const TCHAR _afxSelect[] = _T("SELECT ");
AFX_STATIC_DATA const TCHAR _afxFrom[] = _T(" FROM ");
AFX_STATIC_DATA const TCHAR _afxWhere[] = _T(" WHERE ");
AFX_STATIC_DATA const TCHAR _afxOrderBy[] = _T(" ORDER BY ");
AFX_STATIC_DATA const TCHAR _afxForUpdate[] = _T(" FOR UPDATE ");
AFX_STATIC_DATA const TCHAR _afxRowFetch[] = _T("State:01S01");
AFX_STATIC_DATA const TCHAR _afxDataTruncated[] = _T("State:01004");
AFX_STATIC_DATA const TCHAR _afxInfoRange[] = _T("State:S1096");
AFX_STATIC_DATA const TCHAR _afxOutOfSequence[] = _T("State:S1010");
AFX_STATIC_DATA const TCHAR _afxDriverNotCapable[] = _T("State:S1C00");
AFX_STATIC_DATA const char _afxODBCDLL[] = "ODBC32.DLL";
/////////////////////////////////////////////////////////////////////////////
// for dynamic load of ODBC32.DLL
#pragma comment(lib, "odbc32.lib")
/////////////////////////////////////////////////////////////////////////////
// CDBException
void AFXAPI AfxThrowDBException(RETCODE nRetCode, CDatabase* pdb, HSTMT hstmt)
{
CDBException* pException = new CDBException(nRetCode);
if (nRetCode == SQL_ERROR && pdb != NULL)
pException->BuildErrorString(pdb, hstmt);
else if (nRetCode > AFX_SQL_ERROR && nRetCode < AFX_SQL_ERROR_MAX)
{
VERIFY(pException->m_strError.LoadString(
AFX_IDP_SQL_FIRST+(nRetCode-AFX_SQL_ERROR)));
TRACE1("%s\n", pException->m_strError);
}
THROW(pException);
}
CDBException::CDBException(RETCODE nRetCode)
{
m_nRetCode = nRetCode;
}
CDBException::~CDBException()
{
}
void CDBException::BuildErrorString(CDatabase* pdb, HSTMT hstmt, BOOL bTrace)
{
ASSERT_VALID(this);
UNUSED(bTrace); // unused in release builds
if (pdb != NULL)
{
SWORD nOutlen;
RETCODE nRetCode;
UCHAR lpszMsg[SQL_MAX_MESSAGE_LENGTH];
UCHAR lpszState[SQL_SQLSTATE_SIZE];
CString strMsg;
CString strState;
SDWORD lNative;
_AFX_DB_STATE* pDbState = _afxDbState;
AFX_SQL_SYNC(::SQLError(pDbState->m_henvAllConnections, pdb->m_hdbc,
hstmt, lpszState, &lNative,
lpszMsg, SQL_MAX_MESSAGE_LENGTH-1, &nOutlen));
strState = lpszState;
// Skip non-errors
while ((nRetCode == SQL_SUCCESS || nRetCode == SQL_SUCCESS_WITH_INFO) &&
lstrcmp(strState, _T("00000")) != 0)
{
strMsg = lpszMsg;
TCHAR lpszNative[50];
wsprintf(lpszNative, _T(",Native:%ld,Origin:"), lNative);
strState += lpszNative;
// transfer [origin] from message string to StateNativeOrigin string
int nCloseBracket;
int nMsgLength;
while (!strMsg.IsEmpty() &&
strMsg[0] == '[' && (nCloseBracket = strMsg.Find(']')) >= 0)
{
// Skip ']'
nCloseBracket++;
strState += strMsg.Left(nCloseBracket);
nMsgLength = strMsg.GetLength();
// Skip ' ', if present
if (nCloseBracket < nMsgLength && strMsg[nCloseBracket] == ' ')
nCloseBracket++;
strMsg = strMsg.Right(nMsgLength - nCloseBracket);
}
strState += _T("\n");
m_strStateNativeOrigin += _T("State:") + strState;
m_strError += strMsg + _T("\n");
#ifdef _DEBUG
if (bTrace)
{
TraceErrorMessage(strMsg);
TraceErrorMessage(_T("State:") + strState);
}
#endif // _DEBUG
AFX_SQL_SYNC(::SQLError(pDbState->m_henvAllConnections,
pdb->m_hdbc, hstmt, lpszState, &lNative,
lpszMsg, SQL_MAX_MESSAGE_LENGTH-1, &nOutlen));
strState = lpszState;
}
}
}
BOOL CDBException::GetErrorMessage(LPTSTR lpszError, UINT nMaxError,
PUINT pnHelpContext /* = NULL */)
{
ASSERT(lpszError != NULL && AfxIsValidString(lpszError, nMaxError));
if (pnHelpContext != NULL)
*pnHelpContext = 0;
lstrcpyn(lpszError, m_strError, nMaxError-1);
lpszError[nMaxError-1] = '\0';
return TRUE;
}
#ifdef _DEBUG
void CDBException::TraceErrorMessage(LPCTSTR szTrace) const
{
CString strTrace = szTrace;
if (strTrace.GetLength() <= 80)
TRACE1("%s\n", strTrace);
else
{
// Display 80 chars/line
while (strTrace.GetLength() > 80)
{
TRACE1("%s\n", strTrace.Left(80));
strTrace = strTrace.Right(strTrace.GetLength() - 80);
}
TRACE1("%s\n", strTrace);
}
}
#endif // _DEBUG
void CDBException::Empty()
{
m_strError.Empty();
m_strStateNativeOrigin.Empty();
}
/////////////////////////////////////////////////////////////////////////////
// Global helper
HENV AFXAPI AfxGetHENV()
{
_AFX_DB_STATE* pDbState = _afxDbState;
return pDbState->m_henvAllConnections;
}
/////////////////////////////////////////////////////////////////////////////
// CDatabase implementation
CDatabase::CDatabase()
{
m_hdbc = SQL_NULL_HDBC;
m_hstmt = SQL_NULL_HSTMT;
m_bUpdatable = FALSE;
m_bTransactions = FALSE;
DEBUG_ONLY(m_bTransactionPending = FALSE);
m_dwLoginTimeout = DEFAULT_LOGIN_TIMEOUT;
m_dwQueryTimeout = DEFAULT_QUERY_TIMEOUT;
m_bStripTrailingSpaces = FALSE;
m_bIncRecordCountOnAdd = FALSE;
m_bAddForUpdate = FALSE;
}
CDatabase::~CDatabase()
{
ASSERT_VALID(this);
Free();
}
BOOL CDatabase::Open(LPCTSTR lpszDSN, BOOL bExclusive,
BOOL bReadonly, LPCTSTR lpszConnect, BOOL bUseCursorLib)
{
ASSERT(lpszDSN == NULL || AfxIsValidString(lpszDSN));
ASSERT(lpszConnect == NULL || AfxIsValidString(lpszConnect));
CString strConnect;
if (lpszConnect != NULL)
strConnect = lpszConnect;
// For VB/Access compatibility, require "ODBC;" (or "odbc;")
// prefix to the connect string
if (_tcsnicmp(strConnect, _afxODBCTrail, lstrlen(_afxODBCTrail)) != 0)
{
TRACE0("Error: Missing 'ODBC' prefix on connect string.\n");
return FALSE;
}
// Strip "ODBC;"
strConnect = strConnect.Right(strConnect.GetLength()
- lstrlen(_afxODBCTrail));
if (lpszDSN != NULL && lstrlen(lpszDSN) != 0)
{
// Append "DSN=" lpszDSN
strConnect += _T(";DSN=");
strConnect += lpszDSN;
}
DWORD dwOptions = 0;
if (bExclusive)
dwOptions |= openExclusive;
if (bReadonly)
dwOptions |= openReadOnly;
if (bUseCursorLib)
dwOptions |= useCursorLib;
return OpenEx(strConnect, dwOptions);
}
BOOL CDatabase::OpenEx(LPCTSTR lpszConnectString, DWORD dwOptions)
{
ASSERT_VALID(this);
ASSERT(lpszConnectString == NULL || AfxIsValidString(lpszConnectString));
ASSERT(!(dwOptions & noOdbcDialog && dwOptions & forceOdbcDialog));
// Exclusive access not supported.
ASSERT(!(dwOptions & openExclusive));
m_bUpdatable = !(dwOptions & openReadOnly);
TRY
{
m_strConnect = lpszConnectString;
// Allocate the HDBC and make connection
AllocConnect(dwOptions);
if(!Connect(dwOptions))
return FALSE;
// Verify support for required functionality and cache info
VerifyConnect();
GetConnectInfo();
}
CATCH_ALL(e)
{
Free();
THROW_LAST();
}
END_CATCH_ALL
return TRUE;
}
void CDatabase::ExecuteSQL(LPCTSTR lpszSQL)
{
USES_CONVERSION;
RETCODE nRetCode;
HSTMT hstmt;
ASSERT_VALID(this);
ASSERT(AfxIsValidString(lpszSQL));
AFX_SQL_SYNC(::SQLAllocStmt(m_hdbc, &hstmt));
if (!CheckHstmt(nRetCode, hstmt))
AfxThrowDBException(nRetCode, this, hstmt);
TRY
{
OnSetOptions(hstmt);
// Give derived CDatabase classes option to use parameters
BindParameters(hstmt);
AFX_ODBC_CALL(::SQLExecDirect(hstmt,
(UCHAR*)T2A((LPTSTR)lpszSQL), SQL_NTS));
if (!CheckHstmt(nRetCode, hstmt))
AfxThrowDBException(nRetCode, this, hstmt);
else
{
do
{
SWORD nResultColumns;
AFX_ODBC_CALL(::SQLNumResultCols(hstmt, &nResultColumns));
if (nResultColumns != 0)
{
do
{
AFX_ODBC_CALL(::SQLFetch(hstmt));
} while (CheckHstmt(nRetCode, hstmt) &&
nRetCode != SQL_NO_DATA_FOUND);
}
AFX_ODBC_CALL(::SQLMoreResults(hstmt));
} while (CheckHstmt(nRetCode, hstmt) &&
nRetCode != SQL_NO_DATA_FOUND);
}
}
CATCH_ALL(e)
{
::SQLCancel(hstmt);
AFX_SQL_SYNC(::SQLFreeStmt(hstmt, SQL_DROP));
THROW_LAST();
}
END_CATCH_ALL
AFX_SQL_SYNC(::SQLFreeStmt(hstmt, SQL_DROP));
}
// Shutdown pending query for CDatabase's private m_hstmt
void CDatabase::Cancel()
{
ASSERT_VALID(this);
ASSERT(m_hdbc != SQL_NULL_HDBC);
::SQLCancel(m_hstmt);
}
// Disconnect connection
void CDatabase::Close()
{
ASSERT_VALID(this);
// Close any open recordsets
AfxLockGlobals(CRIT_ODBC);
TRY
{
while (!m_listRecordsets.IsEmpty())
{
CRecordset* pSet = (CRecordset*)m_listRecordsets.GetHead();
pSet->Close(); // will implicitly remove from list
pSet->m_pDatabase = NULL;
}
}
CATCH_ALL(e)
{
AfxUnlockGlobals(CRIT_ODBC);
THROW_LAST();
}
END_CATCH_ALL
AfxUnlockGlobals(CRIT_ODBC);
if (m_hdbc != SQL_NULL_HDBC)
{
RETCODE nRetCode;
AFX_SQL_SYNC(::SQLDisconnect(m_hdbc));
AFX_SQL_SYNC(::SQLFreeConnect(m_hdbc));
m_hdbc = SQL_NULL_HDBC;
_AFX_DB_STATE* pDbState = _afxDbState;
AfxLockGlobals(CRIT_ODBC);
ASSERT(pDbState->m_nAllocatedConnections != 0);
pDbState->m_nAllocatedConnections--;
AfxUnlockGlobals(CRIT_ODBC);
}
}
// Silently disconnect and free all ODBC resources. Don't throw any exceptions
void CDatabase::Free()
{
ASSERT_VALID(this);
// Trap failures upon close
TRY
{
Close();
}
CATCH_ALL(e)
{
// Nothing we can do
TRACE0("Error: exception by CDatabase::Close() ignored in CDatabase::Free().\n");
DELETE_EXCEPTION(e);
}
END_CATCH_ALL
// free henv if refcount goes to 0
_AFX_DB_STATE* pDbState = _afxDbState;
AfxLockGlobals(CRIT_ODBC);
if (pDbState->m_henvAllConnections != SQL_NULL_HENV)
{
ASSERT(pDbState->m_nAllocatedConnections >= 0);
if (pDbState->m_nAllocatedConnections == 0)
{
// free last connection - release HENV
RETCODE nRetCodeEnv = ::SQLFreeEnv(pDbState->m_henvAllConnections);
#ifdef _DEBUG
if (nRetCodeEnv != SQL_SUCCESS)
// Nothing we can do
TRACE0("Error: SQLFreeEnv failure ignored in CDatabase::Free().\n");
#endif
pDbState->m_henvAllConnections = SQL_NULL_HENV;
}
}
AfxUnlockGlobals(CRIT_ODBC);
}
void CDatabase::OnSetOptions(HSTMT hstmt)
{
RETCODE nRetCode;
ASSERT_VALID(this);
ASSERT(m_hdbc != SQL_NULL_HDBC);
if (m_dwQueryTimeout != -1)
{
// Attempt to set query timeout. Ignore failure
AFX_SQL_SYNC(::SQLSetStmtOption(hstmt, SQL_QUERY_TIMEOUT,
m_dwQueryTimeout));
if (!Check(nRetCode))
// don't attempt it again
m_dwQueryTimeout = (DWORD)-1;
}
}
CString CDatabase::GetDatabaseName() const
{
ASSERT_VALID(this);
ASSERT(m_hdbc != SQL_NULL_HDBC);
char szName[MAX_TNAME_LEN];
SWORD nResult;
RETCODE nRetCode;
AFX_SQL_SYNC(::SQLGetInfo(m_hdbc, SQL_DATABASE_NAME,
szName, _countof(szName), &nResult));
if (!Check(nRetCode))
szName[0] = '\0';
return szName;
}
BOOL CDatabase::BeginTrans()
{
ASSERT_VALID(this);
ASSERT(m_hdbc != SQL_NULL_HDBC);
if (!m_bTransactions)
return FALSE;
// Only 1 level of transactions supported
#ifdef _DEBUG
ASSERT(!m_bTransactionPending);
#endif
RETCODE nRetCode;
AFX_SQL_SYNC(::SQLSetConnectOption(m_hdbc, SQL_AUTOCOMMIT,
SQL_AUTOCOMMIT_OFF));
DEBUG_ONLY(m_bTransactionPending = TRUE);
return Check(nRetCode);
}
BOOL CDatabase::CommitTrans()
{
ASSERT_VALID(this);
ASSERT(m_hdbc != SQL_NULL_HDBC);
if (!m_bTransactions)
return FALSE;
// BeginTrans must be called first
#ifdef _DEBUG
ASSERT(m_bTransactionPending);
#endif
_AFX_DB_STATE* pDbState = _afxDbState;
RETCODE nRetCode;
AFX_SQL_SYNC(::SQLTransact(pDbState->m_henvAllConnections, m_hdbc, SQL_COMMIT));
BOOL bSuccess = Check(nRetCode);
// Turn back on auto commit
AFX_SQL_SYNC(::SQLSetConnectOption(m_hdbc, SQL_AUTOCOMMIT,
SQL_AUTOCOMMIT_ON));
DEBUG_ONLY(m_bTransactionPending = FALSE);
return bSuccess;
}
BOOL CDatabase::Rollback()
{
ASSERT_VALID(this);
ASSERT(m_hdbc != SQL_NULL_HDBC);
if (!m_bTransactions)
return FALSE;
// BeginTrans must be called first
#ifdef _DEBUG
ASSERT(m_bTransactionPending);
#endif
_AFX_DB_STATE* pDbState = _afxDbState;
RETCODE nRetCode;
AFX_SQL_SYNC(::SQLTransact(pDbState->m_henvAllConnections, m_hdbc, SQL_ROLLBACK));
BOOL bSuccess = Check(nRetCode);
// Turn back on auto commit
AFX_SQL_SYNC(::SQLSetConnectOption(m_hdbc, SQL_AUTOCOMMIT,
SQL_AUTOCOMMIT_ON));
DEBUG_ONLY(m_bTransactionPending = FALSE);
return bSuccess;
}
// Screen for errors.
BOOL CDatabase::Check(RETCODE nRetCode) const
{
return CheckHstmt(nRetCode, SQL_NULL_HSTMT);
}
BOOL CDatabase::CheckHstmt(RETCODE nRetCode, HSTMT hstmt) const
{
ASSERT_VALID(this);
UNUSED(hstmt);
switch (nRetCode)
{
case SQL_SUCCESS_WITH_INFO:
#ifdef _DEBUG
if (afxTraceFlags & traceDatabase)
{
CDBException e(nRetCode);
TRACE0("Warning: ODBC Success With Info, ");
e.BuildErrorString((CDatabase*)this, hstmt);
}
#endif // _DEBUG
// Fall through
case SQL_SUCCESS:
case SQL_NO_DATA_FOUND:
return TRUE;
}
return FALSE;
}
//////////////////////////////////////////////////////////////////////////////
// CDatabase internal functions
//Replace brackets in SQL string with SQL_IDENTIFIER_QUOTE_CHAR
void CDatabase::ReplaceBrackets(LPTSTR lpchSQL)
{
BOOL bInLiteral = FALSE;
LPTSTR lpchNewSQL = lpchSQL;
while (*lpchSQL != '\0')
{
if (*lpchSQL == _afxLiteralSeparator)
{
// Handle escaped literal
if (*_tcsinc(lpchSQL) == _afxLiteralSeparator)
{
*lpchNewSQL = *lpchSQL;
lpchSQL = _tcsinc(lpchSQL);
lpchNewSQL = _tcsinc(lpchNewSQL);
}
else
bInLiteral = !bInLiteral;
*lpchNewSQL = *lpchSQL;
}
else if (!bInLiteral && (*lpchSQL == '['))
{
if (*_tcsinc(lpchSQL) == '[')
{
// Handle escaped left bracket by inserting one '['
*lpchNewSQL = *lpchSQL;
lpchSQL = _tcsinc(lpchSQL);
}
else
*lpchNewSQL = m_chIDQuoteChar;
}
else if (!bInLiteral && (*lpchSQL == ']'))
{
if (*_tcsinc(lpchSQL) == ']')
{
// Handle escaped right bracket by inserting one ']'
*lpchNewSQL = *lpchSQL;
lpchSQL = _tcsinc(lpchSQL);
}
else
*lpchNewSQL = m_chIDQuoteChar;
}
else
*lpchNewSQL = *lpchSQL;
lpchSQL = _tcsinc(lpchSQL);
lpchNewSQL = _tcsinc(lpchNewSQL);
}
}
// Allocate an henv (first time called) and hdbc
void CDatabase::AllocConnect(DWORD dwOptions)
{
ASSERT_VALID(this);
if (m_hdbc != SQL_NULL_HDBC)
return;
_AFX_DB_STATE* pDbState = _afxDbState;
RETCODE nRetCode;
AfxLockGlobals(CRIT_ODBC);
if (pDbState->m_henvAllConnections == SQL_NULL_HENV)
{
ASSERT(pDbState->m_nAllocatedConnections == 0);
// need to allocate an environment for first connection
AFX_SQL_SYNC(::SQLAllocEnv(&pDbState->m_henvAllConnections));
if (!Check(nRetCode))
{
AfxUnlockGlobals(CRIT_ODBC);
AfxThrowMemoryException(); // fatal
}
}
ASSERT(pDbState->m_henvAllConnections != SQL_NULL_HENV);
AFX_SQL_SYNC(::SQLAllocConnect(pDbState->m_henvAllConnections, &m_hdbc));
if (!Check(nRetCode))
{
AfxUnlockGlobals(CRIT_ODBC);
ThrowDBException(nRetCode); // fatal
}
pDbState->m_nAllocatedConnections++; // allocated at least
AfxUnlockGlobals(CRIT_ODBC);
#ifdef _DEBUG
if (bTraceSql)
{
::SQLSetConnectOption(m_hdbc, SQL_OPT_TRACEFILE,
(DWORD)"odbccall.txt");
::SQLSetConnectOption(m_hdbc, SQL_OPT_TRACE, 1);
}
#endif // _DEBUG
AFX_SQL_SYNC(::SQLSetConnectOption(m_hdbc, SQL_LOGIN_TIMEOUT,
m_dwLoginTimeout));
#ifdef _DEBUG
if (nRetCode != SQL_SUCCESS && nRetCode != SQL_SUCCESS_WITH_INFO &&
(afxTraceFlags & traceDatabase))
TRACE0("Warning: Failure setting login timeout.\n");
#endif
if (!m_bUpdatable)
{
AFX_SQL_SYNC(::SQLSetConnectOption(m_hdbc, SQL_ACCESS_MODE,
SQL_MODE_READ_ONLY));
#ifdef _DEBUG
if (nRetCode != SQL_SUCCESS && nRetCode != SQL_SUCCESS_WITH_INFO &&
(afxTraceFlags & traceDatabase))
TRACE0("Warning: Failure setting read only access mode.\n");
#endif
}
// Turn on cursor lib support
if (dwOptions & useCursorLib)
{
AFX_SQL_SYNC(::SQLSetConnectOption(m_hdbc,
SQL_ODBC_CURSORS, SQL_CUR_USE_ODBC));
// With cursor library added records immediately in result set
m_bIncRecordCountOnAdd = TRUE;
}
}
BOOL CDatabase::Connect(DWORD dwOptions)
{
USES_CONVERSION;
HWND hWndTop;
HWND hWnd = CWnd::GetSafeOwner_(NULL, &hWndTop);
if (hWnd == NULL)
hWnd = ::GetDesktopWindow();
UCHAR szConnectOutput[MAX_CONNECT_LEN];
RETCODE nRetCode;
SWORD nResult;
UWORD wConnectOption = SQL_DRIVER_COMPLETE;
if (dwOptions & noOdbcDialog)
wConnectOption = SQL_DRIVER_NOPROMPT;
else if (dwOptions & forceOdbcDialog)
wConnectOption = SQL_DRIVER_PROMPT;
AFX_SQL_SYNC(::SQLDriverConnect(m_hdbc, hWnd,
(UCHAR*)T2A((LPTSTR)(LPCTSTR)m_strConnect), SQL_NTS,
szConnectOutput, _countof(szConnectOutput),
&nResult, wConnectOption));
if (hWndTop != NULL)
::EnableWindow(hWndTop, TRUE);
// If user hit 'Cancel'
if (nRetCode == SQL_NO_DATA_FOUND)
{
Free();
return FALSE;
}
if (!Check(nRetCode))
{
#ifdef _DEBUG
if (hWnd == NULL)
TRACE0("Error: No default window (AfxGetApp()->m_pMainWnd) for SQLDriverConnect.\n");
#endif
ThrowDBException(nRetCode);
}
// Connect strings must have "ODBC;"
m_strConnect = _afxODBCTrail;
// Save connect string returned from ODBC
m_strConnect += (char*)szConnectOutput;
return TRUE;
}
void CDatabase::VerifyConnect()
{
RETCODE nRetCode;
SWORD nResult;
SWORD nAPIConformance;
AFX_SQL_SYNC(::SQLGetInfo(m_hdbc, SQL_ODBC_API_CONFORMANCE,
&nAPIConformance, sizeof(nAPIConformance), &nResult));
if (!Check(nRetCode))
ThrowDBException(nRetCode);
if (nAPIConformance < SQL_OAC_LEVEL1)
ThrowDBException(AFX_SQL_ERROR_API_CONFORMANCE);
SWORD nSQLConformance;
AFX_SQL_SYNC(::SQLGetInfo(m_hdbc, SQL_ODBC_SQL_CONFORMANCE,
&nSQLConformance, sizeof(nSQLConformance), &nResult));
if (!Check(nRetCode))
ThrowDBException(nRetCode);
if (nSQLConformance < SQL_OSC_MINIMUM)
ThrowDBException(AFX_SQL_ERROR_SQL_CONFORMANCE);
}
void CDatabase::GetConnectInfo()
{
RETCODE nRetCode;
SWORD nResult;
// Reset the database update options
m_dwUpdateOptions = 0;
// Check for SQLSetPos support
UDWORD dwDriverPosOperations;
AFX_SQL_SYNC(::SQLGetInfo(m_hdbc, SQL_POS_OPERATIONS,
&dwDriverPosOperations, sizeof(dwDriverPosOperations), &nResult));
if (Check(nRetCode) &&
(dwDriverPosOperations & SQL_POS_UPDATE) &&
(dwDriverPosOperations & SQL_POS_DELETE) &&
(dwDriverPosOperations & SQL_POS_ADD))
m_dwUpdateOptions |= AFX_SQL_SETPOSUPDATES;
// Check for positioned update SQL support
UDWORD dwPositionedStatements;
AFX_SQL_SYNC(::SQLGetInfo(m_hdbc, SQL_POSITIONED_STATEMENTS,
&dwPositionedStatements, sizeof(dwPositionedStatements),
&nResult));
if (Check(nRetCode) &&
(dwPositionedStatements & SQL_PS_POSITIONED_DELETE) &&
(dwPositionedStatements & SQL_PS_POSITIONED_UPDATE))
m_dwUpdateOptions |= AFX_SQL_POSITIONEDSQL;
// Check for transaction support
SWORD nTxnCapable;
AFX_SQL_SYNC(::SQLGetInfo(m_hdbc, SQL_TXN_CAPABLE, &nTxnCapable,
sizeof(nTxnCapable), &nResult));
if (Check(nRetCode) && nTxnCapable != SQL_TC_NONE)
m_bTransactions = TRUE;
// Cache the effect of transactions on cursors
AFX_SQL_SYNC(::SQLGetInfo(m_hdbc, SQL_CURSOR_COMMIT_BEHAVIOR,
&m_nCursorCommitBehavior, sizeof(m_nCursorCommitBehavior),
&nResult));
if (!Check(nRetCode))
m_nCursorCommitBehavior = SQL_ERROR;
AFX_SQL_SYNC(::SQLGetInfo(m_hdbc, SQL_CURSOR_ROLLBACK_BEHAVIOR,
&m_nCursorRollbackBehavior, sizeof(m_nCursorRollbackBehavior),
&nResult));
if (!Check(nRetCode))
m_nCursorRollbackBehavior = SQL_ERROR;
// Cache bookmark attributes
AFX_SQL_SYNC(::SQLGetInfo(m_hdbc, SQL_BOOKMARK_PERSISTENCE,
&m_dwBookmarkAttributes, sizeof(m_dwBookmarkAttributes),
&nResult));
Check(nRetCode);
// Check for SQLGetData support req'd by RFX_LongBinary
UDWORD dwGetDataExtensions;
AFX_SQL_SYNC(::SQLGetInfo(m_hdbc, SQL_GETDATA_EXTENSIONS,
&dwGetDataExtensions, sizeof(dwGetDataExtensions),
&nResult));
if (!Check(nRetCode))
dwGetDataExtensions = 0;
if (dwGetDataExtensions & SQL_GD_BOUND)
m_dwUpdateOptions |= AFX_SQL_GDBOUND;
if (m_bUpdatable)
{
// Make sure data source is Updatable
char szReadOnly[10];
AFX_SQL_SYNC(::SQLGetInfo(m_hdbc, SQL_DATA_SOURCE_READ_ONLY,
szReadOnly, _countof(szReadOnly), &nResult));
if (Check(nRetCode) && nResult == 1)
m_bUpdatable = !(lstrcmpA(szReadOnly, "Y") == 0);
else
m_bUpdatable = FALSE;
#ifdef _DEBUG
if (!m_bUpdatable && (afxTraceFlags & traceDatabase))
TRACE0("Warning: data source is readonly.\n");
#endif
}
else
{
// Make data source is !Updatable
AFX_SQL_SYNC(::SQLSetConnectOption(m_hdbc,
SQL_ACCESS_MODE, SQL_MODE_READ_ONLY));
}
// Cache the quote char to use when constructing SQL
char szIDQuoteChar[2];
AFX_SQL_SYNC(::SQLGetInfo(m_hdbc, SQL_IDENTIFIER_QUOTE_CHAR,
szIDQuoteChar, _countof(szIDQuoteChar), &nResult));
if (Check(nRetCode) && nResult == 1)
m_chIDQuoteChar = szIDQuoteChar[0];
else
m_chIDQuoteChar = ' ';
#ifdef _DEBUG
if (afxTraceFlags & traceDatabase)
{
char szInfo[64];
AFX_SQL_SYNC(::SQLGetInfo(m_hdbc, SQL_DBMS_NAME,
szInfo, _countof(szInfo), &nResult));
if (Check(nRetCode))
{
CString strInfo = szInfo;
TRACE1("DBMS: %s\n", strInfo);
AFX_SQL_SYNC(::SQLGetInfo(m_hdbc, SQL_DBMS_VER,
szInfo, _countof(szInfo), &nResult));
if (Check(nRetCode))
{
strInfo = szInfo;
TRACE1(", Version: %s\n", strInfo);
}
}
}
#endif // _DEBUG
}
void CDatabase::BindParameters(HSTMT /* hstmt */)
{
// Must override and call SQLBindParameter directly
}
//////////////////////////////////////////////////////////////////////////////
// CDatabase diagnostics
#ifdef _DEBUG
void CDatabase::AssertValid() const
{
CObject::AssertValid();
}
void CDatabase::Dump(CDumpContext& dc) const
{
CObject::Dump(dc);
dc << "m_hdbc = " << m_hdbc;
dc << "\nm_strConnect = " << m_strConnect;
dc << "\nm_bUpdatable = " << m_bUpdatable;
dc << "\nm_bTransactions = " << m_bTransactions;
dc << "\nm_bTransactionPending = " << m_bTransactionPending;
dc << "\nm_dwLoginTimeout = " << m_dwLoginTimeout;
dc << "\nm_dwQueryTimeout = " << m_dwQueryTimeout;
if (dc.GetDepth() > 0)
{
_AFX_DB_STATE* pDbState = _afxDbState;
dc << "\nwith env:";
dc << "\n\tnAllocated = " << pDbState->m_nAllocatedConnections;
dc << "\n\thenvAllConnections = " << pDbState->m_henvAllConnections;
}
dc << "\n";
}
#endif // _DEBUG
//////////////////////////////////////////////////////////////////////////////
// CRecordset helpers
void AFXAPI AfxSetCurrentRecord(long* plCurrentRecord, long nRows, RETCODE nRetCode);
void AFXAPI AfxSetRecordCount(long* plRecordCount, long lCurrentRecord,
BOOL bEOFSeen, RETCODE nRetCode);
//////////////////////////////////////////////////////////////////////////////
// CRecordset
CRecordset::CRecordset(CDatabase* pDatabase)
{
ASSERT(pDatabase == NULL || AfxIsValidAddress(pDatabase, sizeof(CDatabase)));
m_pDatabase = pDatabase;
m_nOpenType = snapshot;
m_lOpen = AFX_RECORDSET_STATUS_UNKNOWN;
m_nEditMode = noMode;
m_nDefaultType = snapshot;
m_dwOptions = none;
m_bAppendable = FALSE;
m_bUpdatable = FALSE;
m_bScrollable = FALSE;
m_bRecordsetDb = FALSE;
m_bRebindParams = FALSE;
m_bLongBinaryColumns = FALSE;
m_nLockMode = optimistic;
m_dwInitialGetDataLen = 0;
m_rgODBCFieldInfos = NULL;
m_rgFieldInfos = NULL;
m_rgRowStatus = NULL;
m_dwRowsetSize = 25;
m_dwAllocatedRowsetSize = 0;
m_nFields = 0;
m_nParams = 0;
m_nFieldsBound = 0;
m_lCurrentRecord = AFX_CURRENT_RECORD_UNDEFINED;
m_lRecordCount = 0;
m_bUseUpdateSQL = FALSE;
m_bUseODBCCursorLib = FALSE;
m_nResultCols = -1;
m_bCheckCacheForDirtyFields = TRUE;
m_pbFieldFlags = NULL;
m_pbParamFlags = NULL;
m_plParamLength = NULL;
m_pvFieldProxy = NULL;
m_pvParamProxy = NULL;
m_nProxyFields = 0;
m_nProxyParams = 0;
m_hstmtUpdate = SQL_NULL_HSTMT;
m_hstmt = SQL_NULL_HSTMT;
if (m_pDatabase != NULL && m_pDatabase->IsOpen())
{
ASSERT_VALID(m_pDatabase);
TRY
{
RETCODE nRetCode;
AFX_SQL_SYNC(::SQLAllocStmt(m_pDatabase->m_hdbc, &m_hstmt));
if (!Check(nRetCode))
ThrowDBException(SQL_INVALID_HANDLE);
// Add to list of CRecordsets with alloced hstmts
AfxLockGlobals(CRIT_ODBC);
TRY
{
m_pDatabase->m_listRecordsets.AddHead(this);
}
CATCH_ALL(e)
{
AfxUnlockGlobals(CRIT_ODBC);
THROW_LAST();
}
END_CATCH_ALL
AfxUnlockGlobals(CRIT_ODBC);
}
CATCH_ALL(e)
{
ASSERT(m_hstmt == SQL_NULL_HSTMT);
DELETE_EXCEPTION(e);
}
END_CATCH_ALL
}
}
CRecordset::~CRecordset()
{
ASSERT_VALID(this);
TRY
{
if (m_hstmt != NULL)
{
#ifdef _DEBUG
if (m_dwOptions & useMultiRowFetch && afxTraceFlags & traceDatabase)
{
TRACE0("\nWARNING: Close called implicitly from destructor.");
TRACE0("\nUse of multi row fetch requires explicit call");
TRACE0("\nto Close or memory leaks will result.\n");
}
#endif
Close();
}
if (m_bRecordsetDb)
delete m_pDatabase;
m_pDatabase = NULL;
}
CATCH_ALL(e)
{
// Nothing we can do
TRACE0("Error: Exception ignored in ~CRecordset().\n");
DELETE_EXCEPTION(e);
}
END_CATCH_ALL
}
BOOL CRecordset::Open(UINT nOpenType, LPCTSTR lpszSQL, DWORD dwOptions)
{
ASSERT(!IsOpen());
ASSERT_VALID(this);
ASSERT(lpszSQL == NULL || AfxIsValidString(lpszSQL));
ASSERT(nOpenType == AFX_DB_USE_DEFAULT_TYPE ||
nOpenType == dynaset || nOpenType == snapshot ||
nOpenType == forwardOnly || nOpenType == dynamic);
ASSERT(!(dwOptions & readOnly && dwOptions & appendOnly));
// Can only use optimizeBulkAdd with appendOnly recordsets
ASSERT((dwOptions & optimizeBulkAdd && dwOptions & appendOnly) ||
!(dwOptions & optimizeBulkAdd));
// forwardOnly recordsets have limited functionality
ASSERT(!(nOpenType == forwardOnly && dwOptions & skipDeletedRecords));
// Cache state info and allocate hstmt
SetState(nOpenType, lpszSQL, dwOptions);
if(!AllocHstmt())
return FALSE;
// Check if bookmarks upported (CanBookmark depends on open DB)
ASSERT(dwOptions & useBookmarks ? CanBookmark() : TRUE);
TRY
{
OnSetOptions(m_hstmt);
// Allocate the field/param status arrays, if necessary
BOOL bUnbound = FALSE;
if (m_nFields > 0 || m_nParams > 0)
AllocStatusArrays();
else
bUnbound = TRUE;
// Build SQL and prep/execute or just execute direct
BuildSQL(lpszSQL);
PrepareAndExecute();
// Cache some field info and prepare the rowset
AllocAndCacheFieldInfo();
AllocRowset();
// If late binding, still need to allocate status arrays
if (bUnbound && (m_nFields > 0 || m_nParams > 0))
AllocStatusArrays();
// Give derived classes a call before binding
PreBindFields();
// Fetch the first row of data
MoveNext();
// If EOF, then result set empty, so set BOF as well
m_bBOF = m_bEOF;
}
CATCH_ALL(e)
{
Close();
THROW_LAST();
}
END_CATCH_ALL
return TRUE;
}
void CRecordset::Close()
{
ASSERT_VALID(this);
// Can't close if database has been deleted
ASSERT(m_pDatabase != NULL);
// This will force a requery for cursor name if reopened.
m_strCursorName.Empty();
if (m_rgFieldInfos != NULL &&
m_nFields > 0 && m_bCheckCacheForDirtyFields)
{
FreeDataCache();
}
FreeRowset();
m_nEditMode = noMode;
delete [] m_rgFieldInfos;
m_rgFieldInfos = NULL;
delete [] m_rgODBCFieldInfos;
m_rgODBCFieldInfos = NULL;
delete [] m_pbFieldFlags;
m_pbFieldFlags = NULL;
delete [] m_pbParamFlags;
m_pbParamFlags = NULL;
if (m_pvFieldProxy != NULL)
{
for (UINT nField = 0; nField < m_nProxyFields; nField++)
delete m_pvFieldProxy[nField];
delete [] m_pvFieldProxy;
m_pvFieldProxy = NULL;
m_nProxyFields = 0;
}
if (m_pvParamProxy != NULL)
{
for (UINT nParam = 0; nParam < m_nProxyParams; nParam++)
delete m_pvParamProxy[nParam];
delete [] m_pvParamProxy;
m_pvParamProxy = NULL;
m_nProxyParams = 0;
}
delete [] m_plParamLength;
m_plParamLength = NULL;
RETCODE nRetCode;
if (m_hstmt != SQL_NULL_HSTMT)
{
AFX_SQL_SYNC(::SQLFreeStmt(m_hstmt, SQL_DROP));
m_hstmt = SQL_NULL_HSTMT;
}
if (m_hstmtUpdate != SQL_NULL_HSTMT)
{
AFX_SQL_SYNC(::SQLFreeStmt(m_hstmtUpdate, SQL_DROP));
m_hstmtUpdate = SQL_NULL_HSTMT;
}
// Remove CRecordset from CDatabase's list
AfxLockGlobals(CRIT_ODBC);
TRY
{
POSITION pos = m_pDatabase->m_listRecordsets.Find(this);
if (pos != NULL)
m_pDatabase->m_listRecordsets.RemoveAt(pos);
#ifdef _DEBUG
else if (afxTraceFlags & traceDatabase)
TRACE0("WARNING: CRecordset not found in m_pDatabase->m_listRecordsets.\n");
#endif
}
CATCH_ALL(e)
{
AfxUnlockGlobals(CRIT_ODBC);
THROW_LAST();
}
END_CATCH_ALL
AfxUnlockGlobals(CRIT_ODBC);
m_lOpen = AFX_RECORDSET_STATUS_CLOSED;
m_bBOF = TRUE;
m_bEOF = TRUE;
m_bDeleted = FALSE;
m_bAppendable = FALSE;
m_bUpdatable = FALSE;
m_bScrollable = FALSE;
m_bRebindParams = FALSE;
m_bLongBinaryColumns = FALSE;
m_nLockMode = optimistic;
m_nFieldsBound = 0;
m_nResultCols = -1;
}
BOOL CRecordset::IsOpen() const
// Note: assumes base class CRecordset::Close called
{
if (m_hstmt == NULL)
return FALSE;
if (m_lOpen == AFX_RECORDSET_STATUS_OPEN)
return TRUE;
RETCODE nRetCode;
SWORD nCols;
AFX_ODBC_CALL(::SQLNumResultCols(m_hstmt, &nCols));
if (!Check(nRetCode))
{
// If function sequence error, CRecordset not open
CDBException* e = new CDBException(nRetCode);
e->BuildErrorString(m_pDatabase, m_hstmt, FALSE);
if (e->m_strStateNativeOrigin.Find(_afxOutOfSequence) >= 0)
{
e->Delete();
return FALSE;
}
else
{
#ifdef _DEBUG
TRACE0("Error: SQLNumResultCols failed during IsOpen().\n");
e->TraceErrorMessage(e->m_strError);
e->TraceErrorMessage(e->m_strStateNativeOrigin);
#endif
THROW(e);
}
}
BOOL bOpen = FALSE;
if (nCols != 0)
bOpen = TRUE;
return bOpen;
}
BOOL CRecordset::IsFieldDirty(void* pv)
{
ASSERT_VALID(this);
ASSERT(!(m_dwOptions & useMultiRowFetch));
if (m_nFields <= 0)
{
ASSERT(FALSE);
return FALSE;
}
// If not in update op fields can't be dirty
// must compare saved and current values
if (m_nEditMode == noMode)
return FALSE;
// Must compare values to find dirty fields if necessary
if (m_bCheckCacheForDirtyFields)
{
if (m_nEditMode == edit)
MarkForUpdate();
else
MarkForAddNew();
}
int nIndex = 0, nIndexEnd;
if (pv == NULL)
nIndexEnd = m_nFields - 1;
else
{
// GetBoundFieldIndex returns 1-based index
nIndex = nIndexEnd = GetBoundFieldIndex(pv) - 1;
// must be address of field member
ASSERT(nIndex >= 0);
}
BOOL bDirty = FALSE;
while (nIndex <= nIndexEnd && !bDirty)
bDirty = IsFieldStatusDirty(nIndex++);
return bDirty;
}
BOOL CRecordset::IsFieldNull(void* pv)
{
ASSERT_VALID(this);
ASSERT(!(m_dwOptions & useMultiRowFetch));
int nIndex;
BOOL bRetVal;
if (pv == NULL)
{
bRetVal = FALSE;
for (nIndex = 0; !bRetVal && nIndex <= int(m_nFields-1); nIndex++)
bRetVal = IsFieldStatusNull((DWORD) nIndex);
}
else
{
nIndex = GetBoundFieldIndex(pv) - 1;
if (nIndex < 0)
ThrowDBException(AFX_SQL_ERROR_FIELD_NOT_FOUND);
bRetVal = IsFieldStatusNull((DWORD) nIndex);
}
return bRetVal;
}
BOOL CRecordset::IsFieldNullable(void* pv)
{
ASSERT_VALID(this);
if (pv == NULL)
{
// Must specify valid column name
ASSERT(FALSE);
return FALSE;
}
int nIndex = GetBoundFieldIndex(pv) - 1;
if (nIndex < 0)
ThrowDBException(AFX_SQL_ERROR_FIELD_NOT_FOUND);
return IsFieldNullable((DWORD)nIndex);
}
BOOL CRecordset::CanBookmark() const
{
ASSERT_VALID(this);
ASSERT(m_pDatabase->IsOpen());
if (!(m_dwOptions & useBookmarks) ||
(m_nOpenType == forwardOnly && !(m_dwOptions & useExtendedFetch)))
return FALSE;
return m_pDatabase->GetBookmarkPersistence() & SQL_BP_SCROLL;
}
void CRecordset::Move(long nRows, WORD wFetchType)
{
ASSERT_VALID(this);
ASSERT(m_hstmt != SQL_NULL_HSTMT);
// First call - fields haven't been bound (m_nFieldsBound will change)
if (m_nFieldsBound == 0)
{
InitRecord();
ResetCursor();
}
if (m_nFieldsBound > 0)
{
// Reset field flags - mark all clean, all non-null
memset(m_pbFieldFlags, 0, m_nFields);
// Clear any edit mode that was set
m_nEditMode = noMode;
}
// Check scrollability, EOF/BOF status
CheckRowsetCurrencyStatus(wFetchType, nRows);
RETCODE nRetCode;
// Fetch the data, skipping deleted records if necessary
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
// Fetch the data and check for errors
nRetCode = FetchData(wFetchType, nRows, &m_dwRowsFetched);
// Set currency status and increment the record counters
SetRowsetCurrencyStatus(nRetCode, wFetchType, nRows, m_dwRowsFetched);
// Need to fixup bound fields in some cases
if (m_nFields > 0 && !IsEOF() && !IsBOF() &&
!(m_dwOptions & useMultiRowFetch))
{
Fixups();
}
}
void CRecordset::CheckRowsetError(RETCODE nRetCode)
{
if (nRetCode == SQL_SUCCESS_WITH_INFO)
{
CDBException e(nRetCode);
// Build the error string but don't send nuisance output to TRACE window
e.BuildErrorString(m_pDatabase, m_hstmt, FALSE);
if (e.m_strStateNativeOrigin.Find(_afxDataTruncated) >= 0)
{
// Ignore data truncated warning if binding long binary columns
// (may mask non-long binary truncation warnings or other warnings)
if (!((m_pDatabase->m_dwUpdateOptions & AFX_SQL_SETPOSUPDATES) &&
m_bLongBinaryColumns))
{
NO_CPP_EXCEPTION(e.Empty());
TRACE0("Error: field data truncated during data fetch.\n");
ThrowDBException(AFX_SQL_ERROR_DATA_TRUNCATED);
}
}
else if (e.m_strStateNativeOrigin.Find(_afxRowFetch) >= 0)
{
#ifdef _DEBUG
TRACE0("Error: fetching row from server.\n");
e.TraceErrorMessage(e.m_strError);
e.TraceErrorMessage(e.m_strStateNativeOrigin);
#endif
NO_CPP_EXCEPTION(e.Empty());
ThrowDBException(AFX_SQL_ERROR_ROW_FETCH);
}
else
{
#ifdef _DEBUG
// Not a truncation or row fetch warning so send debug output
if (afxTraceFlags & traceDatabase)
{
TRACE0("Warning: ODBC Success With Info,\n");
e.TraceErrorMessage(e.m_strError);
e.TraceErrorMessage(e.m_strStateNativeOrigin);
}
#endif // _DEBUG
}
}
else if (!Check(nRetCode))
ThrowDBException(nRetCode);
}
void CRecordset::GetBookmark(CDBVariant& varBookmark)
{
ASSERT_VALID(this);
// Validate bookmarks are usable
if (!(m_dwOptions & useBookmarks))
ThrowDBException(AFX_SQL_ERROR_BOOKMARKS_NOT_ENABLED);
else if (!CanBookmark())
ThrowDBException(AFX_SQL_ERROR_BOOKMARKS_NOT_SUPPORTED);
// Currently ODBC only supports 4 byte bookmarks
// Initialize the variant to a long
if (varBookmark.m_dwType != DBVT_LONG)
{
varBookmark.Clear();
varBookmark.m_dwType = DBVT_LONG;
varBookmark.m_lVal = 0;
}
RETCODE nRetCode;
SDWORD nActualSize;
// Retrieve the bookmark (column 0) data
AFX_ODBC_CALL(::SQLGetData(m_hstmt, 0, SQL_C_BOOKMARK,
&varBookmark.m_lVal, sizeof(varBookmark.m_lVal), &nActualSize));
if (!Check(nRetCode))
{
TRACE0("Error: GetBookmark operation failed.\n");
ThrowDBException(nRetCode);
}
}
void CRecordset::SetBookmark(const CDBVariant& varBookmark)
{
ASSERT_VALID(this);
// Validate bookmarks are usable
if (!(m_dwOptions & useBookmarks))
ThrowDBException(AFX_SQL_ERROR_BOOKMARKS_NOT_ENABLED);
else if (!CanBookmark())
ThrowDBException(AFX_SQL_ERROR_BOOKMARKS_NOT_SUPPORTED);
// Currently ODBC only supports 4 byte bookmarks
ASSERT(varBookmark.m_dwType == DBVT_LONG);
Move(varBookmark.m_lVal, SQL_FETCH_BOOKMARK);
}
void CRecordset::SetRowsetSize(DWORD dwNewRowsetSize)
{
ASSERT_VALID(this);
ASSERT(dwNewRowsetSize > 0);
// If not yet open, only set expected length
if (!IsOpen())
{
m_dwRowsetSize = dwNewRowsetSize;
return;
}
if (!(m_dwOptions & useMultiRowFetch))
{
// Only works if bulk row fetching!
ASSERT(FALSE);
return;
}
// Need to reallocate some memory if rowset size grows
if (m_dwAllocatedRowsetSize == 0 ||
(m_dwAllocatedRowsetSize < dwNewRowsetSize))
{
// If rowset already allocated, delete old and reallocate
FreeRowset();
m_rgRowStatus = new WORD[dwNewRowsetSize];
// If not a user allocated buffer grow the data buffers
if (!(m_dwOptions & userAllocMultiRowBuffers))
{
// Allocate the rowset field buffers
m_dwRowsetSize = dwNewRowsetSize;
CFieldExchange fx(CFieldExchange::AllocMultiRowBuffer, this);
DoBulkFieldExchange(&fx);
m_dwAllocatedRowsetSize = dwNewRowsetSize;
// Set bound fields to zero, rebind and reset bound field count
int nOldFieldsBound = m_nFieldsBound;
m_nFieldsBound = 0;
InitRecord();
m_nFieldsBound = nOldFieldsBound;
}
}
else
{
// Just reset the new rowset size
m_dwRowsetSize = dwNewRowsetSize;
}
RETCODE nRetCode;
AFX_SQL_SYNC(::SQLSetStmtOption(m_hstmt, SQL_ROWSET_SIZE,
m_dwRowsetSize));
}
void CRecordset::AddNew()
{
ASSERT_VALID(this);
ASSERT(m_hstmt != SQL_NULL_HSTMT);
// we can't construct an INSERT statement w/o any columns
ASSERT(m_nFields != 0);
if (!m_bAppendable)
{
ThrowDBException(AFX_SQL_ERROR_RECORDSET_READONLY);
}
if (m_dwOptions & useMultiRowFetch)
{
// Can't use update methods on multi-row rowset
ASSERT(FALSE);
return;
}
if (m_bCheckCacheForDirtyFields && m_nFields > 0)
{
if (m_nEditMode == noMode)
{
// First addnew call, cache record values
StoreFields();
}
else
{
// subsequent Edit/AddNew call. Restore values, save them again
LoadFields();
StoreFields();
}
}
SetFieldNull(NULL);
SetFieldDirty(NULL, FALSE);
m_nEditMode = addnew;
}
void CRecordset::Edit()
{
ASSERT_VALID(this);
ASSERT(m_hstmt != SQL_NULL_HSTMT);
// we can't construct an UPDATE statement w/o any columns
ASSERT(m_nFields != 0);
if (!m_bUpdatable)
ThrowDBException(AFX_SQL_ERROR_RECORDSET_READONLY);
if (m_dwOptions & useMultiRowFetch)
{
// Can't use update methods on multi-row rowset
ASSERT(FALSE);
return;
}
if (m_bEOF || m_bBOF || m_bDeleted)
{
TRACE0("Error: Edit attempt failed - not on a record.\n");
ThrowDBException(AFX_SQL_ERROR_NO_CURRENT_RECORD);
}
if ((m_nOpenType == dynaset || m_nOpenType == dynamic) &&
m_nLockMode == pessimistic)
{
RETCODE nRetCode;
AFX_ODBC_CALL(::SQLSetPos(m_hstmt, 1, SQL_POSITION,
SQL_LCK_EXCLUSIVE));
if (!Check(nRetCode))
{
TRACE0("Error: attempt to lock record failed during Edit function.\n");
ThrowDBException(nRetCode);
}
}
if (m_bCheckCacheForDirtyFields && m_nFields > 0)
{
if (m_nEditMode == noMode)
// First edit call, cache record values
StoreFields();
else
{
// subsequent Edit/AddNew call. Restore values, save them again
LoadFields();
StoreFields();
}
}
m_nEditMode = edit;
}
BOOL CRecordset::Update()
{
ASSERT_VALID(this);
ASSERT(m_hstmt != SQL_NULL_HSTMT);
if (m_dwOptions & useMultiRowFetch)
{
// Can't use update methods on multi-row rowset
ASSERT(FALSE);
return FALSE;
}
if (m_nEditMode != addnew && m_nEditMode != edit)
{
TRACE0("Error: must enter Edit or AddNew mode before updating.\n");
ThrowDBException(AFX_SQL_ERROR_ILLEGAL_MODE);
}
return UpdateInsertDelete();
}
void CRecordset::Delete()
{
ASSERT_VALID(this);
ASSERT(m_hstmt != SQL_NULL_HSTMT);
if (m_dwOptions & useMultiRowFetch)
{
// Can't use update methods on multi-row rowset
ASSERT(FALSE);
return;
}
if (m_nEditMode != noMode)
{
TRACE0("Error: attempted to delete while still in Edit or AddNew mode.\n");
ThrowDBException(AFX_SQL_ERROR_ILLEGAL_MODE);
}
UpdateInsertDelete(); // This call can't fail in delete mode (noMode)
}
void CRecordset::CancelUpdate()
{
ASSERT_VALID(this);
ASSERT(IsOpen());
if (m_nEditMode == noMode)
// Do nothing if not in edit mode
return;
else
// Reset the edit mode
m_nEditMode = noMode;
// Restore cache if necessary
if (m_bCheckCacheForDirtyFields && m_nFields > 0)
LoadFields();
}
BOOL CRecordset::FlushResultSet() const
{
RETCODE nRetCode;
AFX_ODBC_CALL(::SQLMoreResults(m_hstmt));
if (!Check(nRetCode))
{
TRACE0("Error: attempt FlushResultSet failed.\n");
AfxThrowDBException(nRetCode, m_pDatabase, m_hstmt);
}
// Reset state of cursor
((CRecordset*)this)->ResetCursor();
return nRetCode != SQL_NO_DATA_FOUND;
}
void CRecordset::GetODBCFieldInfo(LPCTSTR lpszName,
CODBCFieldInfo& fieldinfo)
{
ASSERT_VALID(this);
ASSERT(IsOpen());
ASSERT(lpszName != NULL);
// No data or no column info fetched yet
if (GetODBCFieldCount() <= 0)
{
ASSERT(FALSE);
return;
}
// Get the index of the field corresponding to name
short nField = GetFieldIndexByName(lpszName);
GetODBCFieldInfo(nField, fieldinfo);
}
void CRecordset::GetODBCFieldInfo(short nIndex,
CODBCFieldInfo& fieldinfo)
{
ASSERT_VALID(this);
ASSERT(IsOpen());
// No data or no column info fetched yet
if (GetODBCFieldCount() <= 0)
{
ASSERT(FALSE);
return;
}
// Just copy the data into the field info
CODBCFieldInfo* pInfo = &m_rgODBCFieldInfos[nIndex];
fieldinfo.m_strName = pInfo->m_strName;
fieldinfo.m_nSQLType = pInfo->m_nSQLType;
fieldinfo.m_nPrecision = pInfo->m_nPrecision;
fieldinfo.m_nScale = pInfo->m_nScale;
fieldinfo.m_nNullability = pInfo->m_nNullability;
}
void CRecordset::GetFieldValue(LPCTSTR lpszName,
CDBVariant& varValue, short nFieldType)
{
ASSERT_VALID(this);
ASSERT(IsOpen());
ASSERT(lpszName != NULL);
// No data or no column info fetched yet
if (GetODBCFieldCount() <= 0)
{
ASSERT(FALSE);
varValue.Clear();
return;
}
// Get the index of the field corresponding to name
short nField = GetFieldIndexByName(lpszName);
GetFieldValue(nField, varValue, nFieldType);
}
void CRecordset::GetFieldValue(short nIndex,
CDBVariant& varValue, short nFieldType)
{
ASSERT_VALID(this);
ASSERT(IsOpen());
// Clear the previous variant
varValue.Clear();
// No data or no column info fetched yet
if (GetODBCFieldCount() <= 0)
{
ASSERT(FALSE);
return;
}
// Convert index to 1-based and check range
nIndex++;
if (nIndex < 1 || nIndex > GetODBCFieldCount())
{
ThrowDBException(AFX_SQL_ERROR_FIELD_NOT_FOUND);
}
void* pvData = NULL;
int nLen = 0;
// Determine the default field type and get the data buffer
if (nFieldType == DEFAULT_FIELD_TYPE)
{
nFieldType =
GetDefaultFieldType(m_rgODBCFieldInfos[nIndex - 1].m_nSQLType);
}
pvData = GetDataBuffer(varValue, nFieldType, &nLen,
m_rgODBCFieldInfos[nIndex - 1].m_nSQLType,
m_rgODBCFieldInfos[nIndex - 1].m_nPrecision);
// Now can actually get the data
long nActualSize = GetData(m_pDatabase, m_hstmt, nIndex,
nFieldType, pvData, nLen,
m_rgODBCFieldInfos[nIndex - 1].m_nSQLType);
// Handle NULL data separately
if (nActualSize == SQL_NULL_DATA)
{
// Clear value and set the value NULL
varValue.Clear();
}
else
{
// May need to cleanup and call SQLGetData again if LONG_VAR data
if (nFieldType == SQL_C_CHAR)
{
GetLongCharDataAndCleanup(m_pDatabase, m_hstmt, nIndex,
nActualSize, &pvData, nLen, *varValue.m_pstring,
m_rgODBCFieldInfos[nIndex - 1].m_nSQLType);
#ifdef _UNICODE
// Now must convert string to UNICODE
LPCSTR lpszOld = (LPCSTR)varValue.m_pstring->GetBuffer(0);
CString* pStringNew = new CString(lpszOld);
delete varValue.m_pstring;
varValue.m_pstring = pStringNew;
#endif // _UNICODE
}
else if (nFieldType == SQL_C_BINARY)
{
GetLongBinaryDataAndCleanup(m_pDatabase, m_hstmt, nIndex,
nActualSize, &pvData, nLen, varValue,
m_rgODBCFieldInfos[nIndex - 1].m_nSQLType);
}
}
}
void CRecordset::GetFieldValue(LPCTSTR lpszName, CString& strValue)
{
ASSERT_VALID(this);
ASSERT(IsOpen());
ASSERT(lpszName != NULL);
// No data or no column info fetched yet
if (GetODBCFieldCount() <= 0)
{
ASSERT(FALSE);
return;
}
// Get the index of the field corresponding to name
short nField = GetFieldIndexByName(lpszName);
GetFieldValue(nField, strValue);
}
void CRecordset::GetFieldValue(short nIndex, CString& strValue)
{
ASSERT_VALID(this);
ASSERT(IsOpen());
// No data or no column info fetched yet
if (GetODBCFieldCount() <= 0)
{
ASSERT(FALSE);
return;
}
// Convert index to 1-based and check range
nIndex++;
if (nIndex < 1 || nIndex > GetODBCFieldCount())
{
ThrowDBException(AFX_SQL_ERROR_FIELD_NOT_FOUND);
}
int nLen = GetTextLen(m_rgODBCFieldInfos[nIndex - 1].m_nSQLType,
m_rgODBCFieldInfos[nIndex - 1].m_nPrecision);
#ifndef _UNICODE
CString& strData = strValue;
#else
CString strProxy;
CString& strData = strProxy;
#endif
void* pvData = strData.GetBufferSetLength(nLen);
// Now can actually get the data
long nActualSize = GetData(m_pDatabase, m_hstmt, nIndex,
SQL_C_CHAR, pvData, nLen,
m_rgODBCFieldInfos[nIndex - 1].m_nSQLType);
// Handle NULL data separately
if (nActualSize == SQL_NULL_DATA)
{
// Clear value
strValue.Empty();
}
else
{
// May need to cleanup and call SQLGetData again if necessary
GetLongCharDataAndCleanup(m_pDatabase, m_hstmt, nIndex,
nActualSize, &pvData, nLen, strData,
m_rgODBCFieldInfos[nIndex - 1].m_nSQLType);
#ifdef _UNICODE
// Now must convert string to UNICODE
strValue = (LPCSTR)strData.GetBuffer(0);
#endif // _UNIOCDE
}
}
void CRecordset::SetFieldDirty(void* pv, BOOL bDirty)
{
ASSERT_VALID(this);
int nIndex, nIndexEnd;
// If not setting all NULL, check simple case
if (pv != NULL)
{
// GetBoundFieldIndex returns 1-based index
nIndex = GetBoundFieldIndex(pv) - 1;
if (nIndex < 0)
{
// pv must be address of field member
ASSERT(FALSE);
return;
}
else
{
nIndexEnd = nIndex;
}
}
else
{
nIndex = 0;
nIndexEnd = m_nFields - 1;
}
while (nIndex <= nIndexEnd)
{
if (bDirty)
SetDirtyFieldStatus((DWORD)nIndex);
else
ClearDirtyFieldStatus((DWORD)nIndex);
nIndex++;
}
}
void CRecordset::SetFieldNull(void* pv, BOOL bNull)
{
ASSERT_VALID(this);
ASSERT(IsOpen());
ASSERT(!(m_dwOptions & useMultiRowFetch));
// If not setting all fields NULL, check simple case (param) first
if (pv != NULL)
{
// Cached index is 1-based
int nIndex = GetBoundParamIndex(pv) - 1;
if (nIndex >= 0)
{
if (bNull)
SetNullParamStatus(nIndex);
else
ClearNullParamStatus(nIndex);
return;
}
}
// Not a param, must be a field
if (m_nFields <= 0)
{
ASSERT(FALSE);
return;
}
// Need field exchange mechanism to set PSEUDO NULL values
// and to reset data lengths (especially for RFX_LongBinary)
CFieldExchange fx(CFieldExchange::SetFieldNull, this, pv);
fx.m_nFieldFound = 0;
fx.m_bField = bNull;
DoFieldExchange(&fx);
// If no field found, m_nFieldFound will still be zero
ASSERT(fx.m_nFieldFound != 0);
}
void CRecordset::SetParamNull(int nIndex, BOOL bNull)
{
ASSERT_VALID(this);
ASSERT((DWORD)nIndex < m_nParams);
// Can be called before Open, but need to alloc status arrays first
if (!IsOpen())
AllocStatusArrays();
if (bNull)
SetNullParamStatus(nIndex);
else
ClearNullParamStatus(nIndex);
return;
}
void CRecordset::SetLockingMode(UINT nLockMode)
{
if (nLockMode == pessimistic)
{
RETCODE nRetCode;
UDWORD dwTypes;
SWORD nResult;
AFX_SQL_SYNC(::SQLGetInfo(m_pDatabase->m_hdbc, SQL_LOCK_TYPES,
&dwTypes, sizeof(dwTypes), &nResult));
if (!Check(nRetCode) || !(dwTypes & SQL_LCK_EXCLUSIVE))
ThrowDBException(AFX_SQL_ERROR_LOCK_MODE_NOT_SUPPORTED);
}
m_nLockMode = nLockMode;
}
BOOL CRecordset::Requery()
{
RETCODE nRetCode;
ASSERT_VALID(this);
ASSERT(IsOpen());
// Can't requery if using direct execution
if (m_dwOptions & executeDirect)
return FALSE;
TRY
{
// Detect changes to filter and sort
if ((m_strFilter != m_strRequeryFilter) || (m_strSort != m_strRequerySort))
{
m_strRequeryFilter = m_strFilter;
m_strRequerySort = m_strSort;
Close();
if (m_strRequerySQL.IsEmpty())
return Open(m_nOpenType, NULL, m_dwOptions);
else
return Open(m_nOpenType, m_strRequerySQL, m_dwOptions);
}
else
{
// Shutdown current query, preserving buffers for performance
AFX_SQL_SYNC(::SQLFreeStmt(m_hstmt, SQL_CLOSE));
m_lOpen = AFX_RECORDSET_STATUS_CLOSED;
// Rebind date/time parameters
RebindParams(m_hstmt);
// now attempt to re-execute the SQL Query
AFX_ODBC_CALL(::SQLExecute(m_hstmt));
if (!Check(nRetCode))
{
TRACE0("Error: Requery attempt failed.\n");
ThrowDBException(nRetCode);
}
m_lOpen = AFX_RECORDSET_STATUS_OPEN;
// Reset some cursor properties and fetch first record
ResetCursor();
MoveNext();
// If EOF, then result set empty, so set BOF as well
m_bBOF = m_bEOF;
}
}
CATCH_ALL(e)
{
Close();
THROW_LAST();
}
END_CATCH_ALL
return TRUE; // all set
}
// Shutdown any pending query for CRecordset's hstmt's
void CRecordset::Cancel()
{
ASSERT_VALID(this);
ASSERT(m_hstmt != SQL_NULL_HSTMT);
::SQLCancel(m_hstmt);
// If Update hstmt has been allocated, shut it down also
if (m_hstmtUpdate != SQL_NULL_HSTMT)
::SQLCancel(m_hstmtUpdate);
}
CString CRecordset::GetDefaultConnect()
{
ASSERT_VALID(this);
return _afxODBCTrail;
}
CString CRecordset::GetDefaultSQL()
{
ASSERT_VALID(this);
// Override and add table name or entire SQL SELECT statement
return _T("");
}
void CRecordset::DoFieldExchange(CFieldExchange* /* pFX */)
{
ASSERT_VALID(this);
// Do nothing if dynamically retrieving unbound fields,
// otherwise override CRecordset and add RFX calls
}
void CRecordset::DoBulkFieldExchange(CFieldExchange* /* pFX */)
{
ASSERT_VALID(this);
// To use multi-record data fetching, you must use
// a derived CRecordset class and call Close explicitly.
ASSERT(FALSE);
}
void CRecordset::OnSetOptions(HSTMT hstmt)
{
ASSERT_VALID(this);
ASSERT(hstmt != SQL_NULL_HSTMT);
// Inherit options settings from CDatabase
m_pDatabase->OnSetOptions(hstmt);
// If fowardOnly recordset and not using SQLExtendedFetch, quit now
if (m_nOpenType == forwardOnly && !(m_dwOptions & useExtendedFetch))
return;
// Turn on bookmark support if necessary
EnableBookmarks();
// If using forwardOnly and extended fetch, quit now
if (m_nOpenType == forwardOnly)
return;
// Make sure driver supports extended fetch, ODBC 2.0 and requested cursor type
VerifyDriverBehavior();
DWORD dwScrollType = VerifyCursorSupport();
// Set the update method, concurrency and cursor type
SetUpdateMethod();
SetConcurrencyAndCursorType(hstmt, dwScrollType);
}
// Screen for errors.
BOOL CRecordset::Check(RETCODE nRetCode) const
{
ASSERT_VALID(this);
switch (nRetCode)
{
case SQL_SUCCESS_WITH_INFO:
#ifdef _DEBUG
if (afxTraceFlags & traceDatabase)
{
CDBException e(nRetCode);
TRACE0("Warning: ODBC Success With Info, ");
e.BuildErrorString(m_pDatabase, m_hstmt);
}
#endif
// Fall through
case SQL_SUCCESS:
case SQL_NO_DATA_FOUND:
case SQL_NEED_DATA:
return TRUE;
}
return FALSE;
}
void CRecordset::PreBindFields()
{
// Do nothing
}
//////////////////////////////////////////////////////////////////////////////
// CRecordset internal functions
// Cache state information internally in CRecordset
void CRecordset::SetState(int nOpenType, LPCTSTR lpszSQL, DWORD dwOptions)
{
if (nOpenType == AFX_DB_USE_DEFAULT_TYPE)
m_nOpenType = m_nDefaultType;
else
m_nOpenType = nOpenType;
m_bAppendable = (dwOptions & appendOnly) != 0 ||
(dwOptions & readOnly) == 0;
m_bUpdatable = (dwOptions & readOnly) == 0 &&
(dwOptions & appendOnly) == 0;
// Can turn off dirty field checking via dwOptions
if (dwOptions & noDirtyFieldCheck || dwOptions & useMultiRowFetch)
m_bCheckCacheForDirtyFields = FALSE;
// Set recordset readOnly if forwardOnly
if (m_nOpenType == forwardOnly && !(dwOptions & readOnly))
{
#ifdef _DEBUG
if (afxTraceFlags & traceDatabase)
TRACE0("Warning: Setting forwardOnly recordset readOnly.\n");
#endif
dwOptions |= readOnly;
// If using multiRowFetch also set useExtendFetch
if (dwOptions & useMultiRowFetch)
dwOptions |= useExtendedFetch;
}
// Archive info for use in Requery
m_dwOptions = dwOptions;
m_strRequerySQL = lpszSQL;
m_strRequeryFilter = m_strFilter;
m_strRequerySort = m_strSort;
}
// Allocate the Hstmt and implicitly create and open Database if necessary
BOOL CRecordset::AllocHstmt()
{
RETCODE nRetCode;
if (m_hstmt == SQL_NULL_HSTMT)
{
CString strDefaultConnect;
TRY
{
if (m_pDatabase == NULL)
{
m_pDatabase = new CDatabase();
m_bRecordsetDb = TRUE;
}
strDefaultConnect = GetDefaultConnect();
// If not already opened, attempt to open
if (!m_pDatabase->IsOpen())
{
BOOL bUseCursorLib = m_bUseODBCCursorLib;
// If non-readOnly snapshot request must use cursor lib
if (m_nOpenType == snapshot && !(m_dwOptions & readOnly))
{
// This assumes drivers only support readOnly snapshots
bUseCursorLib = TRUE;
}
if (!m_pDatabase->Open(_T(""), FALSE, FALSE,
strDefaultConnect, bUseCursorLib))
{
return FALSE;
}
// If snapshot cursor requested and not supported, load cursor lib
if (m_nOpenType == snapshot && !bUseCursorLib)
{
// Get the supported cursor types
RETCODE nResult;
UDWORD dwDriverScrollOptions;
AFX_SQL_SYNC(::SQLGetInfo(m_pDatabase->m_hdbc, SQL_SCROLL_OPTIONS,
&dwDriverScrollOptions, sizeof(dwDriverScrollOptions), &nResult));
if (!Check(nRetCode))
{
TRACE0("Error: ODBC failure checking for driver capabilities.\n");
ThrowDBException(nRetCode);
}
// Check for STATIC cursor support and load cursor lib
if (!(dwDriverScrollOptions & SQL_SO_STATIC))
{
m_pDatabase->Close();
if (!m_pDatabase->Open(_T(""), FALSE, FALSE,
strDefaultConnect, TRUE))
return FALSE;
}
}
}
AFX_SQL_SYNC(::SQLAllocStmt(m_pDatabase->m_hdbc, &m_hstmt));
if (!Check(nRetCode))
ThrowDBException(SQL_INVALID_HANDLE);
// Add to list of CRecordsets with alloced hstmts
AfxLockGlobals(CRIT_ODBC);
TRY
{
m_pDatabase->m_listRecordsets.AddHead(this);
}
CATCH_ALL(e)
{
AfxUnlockGlobals(CRIT_ODBC);
THROW_LAST();
}
END_CATCH_ALL
AfxUnlockGlobals(CRIT_ODBC);
}
CATCH_ALL(e)
{
#ifdef _DEBUG
if (afxTraceFlags & traceDatabase)
TRACE0("Error: CDatabase create for CRecordset failed.\n");
#endif
NO_CPP_EXCEPTION(strDefaultConnect.Empty());
if (m_bRecordsetDb)
{
delete m_pDatabase;
m_pDatabase = NULL;
}
ASSERT(m_hstmt == SQL_NULL_HSTMT);
THROW_LAST();
}
END_CATCH_ALL
}
return TRUE;
}
// Initialize the status arrays and create the SQL
void CRecordset::BuildSQL(LPCTSTR lpszSQL)
{
if (lpszSQL == NULL)
m_strSQL = GetDefaultSQL();
else
m_strSQL = lpszSQL;
// Set any supplied params
if (m_nParams != 0)
{
UINT nParams = BindParams(m_hstmt);
ASSERT(nParams == m_nParams);
}
// Construct the SQL string
BuildSelectSQL();
AppendFilterAndSortSQL();
// Do some extra checking if trying to set recordset updatable or appendable
if ((m_bUpdatable || m_bAppendable) && !IsRecordsetUpdatable())
m_bUpdatable = m_bAppendable = FALSE;
if (m_bUpdatable && m_bUseUpdateSQL && m_pDatabase->m_bAddForUpdate)
m_strSQL += _afxForUpdate;
// Replace brackets with SQL_IDENTIFIER_QUOTE_CHAR
m_pDatabase->ReplaceBrackets(m_strSQL.GetBuffer(0));
m_strSQL.ReleaseBuffer();
}
// Prepare and Execute the SQL or simple call SQLExecDirect, resetting concurrency if necessary
void CRecordset::PrepareAndExecute()
{
USES_CONVERSION;
RETCODE nRetCode = 0;
BOOL bConcurrency = FALSE;
LPCSTR lpszWSQL = T2CA(m_strSQL);
while (!bConcurrency)
{
// Prepare or execute the query
if (m_dwOptions & executeDirect)
{
AFX_ODBC_CALL(::SQLExecDirect(m_hstmt,
(UCHAR*)lpszWSQL, SQL_NTS));
}
else
{
AFX_ODBC_CALL(::SQLPrepare(m_hstmt,
(UCHAR*)lpszWSQL, SQL_NTS));
}
if (Check(nRetCode))
bConcurrency = TRUE;
else
{
// If "Driver Not Capable" error, assume cursor type doesn't
// support requested concurrency and try alternate concurrency.
CDBException* e = new CDBException(nRetCode);
e->BuildErrorString(m_pDatabase, m_hstmt);
if (m_dwConcurrency != SQL_CONCUR_READ_ONLY &&
e->m_strStateNativeOrigin.Find(_afxDriverNotCapable) >= 0)
{
#ifdef _DEBUG
if (afxTraceFlags & traceDatabase)
TRACE0("Warning: Driver does not support requested concurrency.\n");
#endif
// Don't need exception to persist while attempting to reset concurrency
e->Delete();
// ODBC will automatically attempt to set alternate concurrency if
// request fails, but it won't try LOCK even if driver supports it.
if ((m_dwDriverConcurrency & SQL_SCCO_LOCK) &&
(m_dwConcurrency == SQL_CONCUR_ROWVER ||
m_dwConcurrency == SQL_CONCUR_VALUES))
{
m_dwConcurrency = SQL_CONCUR_LOCK;
}
else
{
m_dwConcurrency = SQL_CONCUR_READ_ONLY;
m_bUpdatable = m_bAppendable = FALSE;
#ifdef _DEBUG
if (afxTraceFlags & traceDatabase)
TRACE0("Warning: Setting recordset read only.\n");
#endif
}
// Attempt to reset the concurrency model.
AFX_SQL_SYNC(::SQLSetStmtOption(m_hstmt, SQL_CONCURRENCY,
m_dwConcurrency));
if (!Check(nRetCode))
{
TRACE0("Error: ODBC failure setting recordset concurrency.\n");
ThrowDBException(nRetCode);
}
}
else
{
TRACE0("Error: ODBC failure on SQLPrepare or SQLExecDirect\n");
THROW(e);
}
}
}
// now attempt to execute the SQL Query if not executed already
if (!(m_dwOptions & executeDirect))
{
AFX_ODBC_CALL(::SQLExecute(m_hstmt));
if (!Check(nRetCode))
ThrowDBException(nRetCode);
}
m_lOpen = AFX_RECORDSET_STATUS_OPEN;
// SQLExecute or SQLExecDirect may have changed an option value
if (nRetCode == SQL_SUCCESS_WITH_INFO)
{
// Check if concurrency was changed in order to mark
// recordset non-updatable if necessary
DWORD dwConcurrency;
AFX_SQL_SYNC(::SQLGetStmtOption(m_hstmt, SQL_CONCURRENCY, &dwConcurrency));
if (!Check(nRetCode))
ThrowDBException(nRetCode);
if (dwConcurrency == SQL_CONCUR_READ_ONLY && (m_bUpdatable || m_bAppendable))
{
m_bUpdatable = FALSE;
m_bAppendable = FALSE;
#ifdef _DEBUG
if (afxTraceFlags & traceDatabase)
{
TRACE0("Warning: Concurrency changed by driver.\n");
TRACE0("\tMarking CRecordset as not updatable.\n");
}
#endif // _DEBUG
}
}
}
// Ensure that driver supports extended fetch and ODBC 2.0 if necessary
void CRecordset::VerifyDriverBehavior()
{
RETCODE nRetCode;
UWORD wScrollable;
// If SQLExtendedFetch not supported, use SQLFetch
AFX_SQL_SYNC(::SQLGetFunctions(m_pDatabase->m_hdbc,
SQL_API_SQLEXTENDEDFETCH, &wScrollable));
if (!Check(nRetCode))
{
TRACE0("Error: ODBC failure determining whether recordset is scrollable.\n");
ThrowDBException(nRetCode);
}
m_bScrollable = wScrollable;
if (!m_bScrollable)
{
#ifdef _DEBUG
if (afxTraceFlags & traceDatabase)
{
TRACE0("Warning: SQLExtendedFetch not supported by driver\n");
TRACE0("and/or cursor library not loaded. Opening forwardOnly.\n");
TRACE0("for use with SQLFetch.\n");
}
#endif
m_bUpdatable = FALSE;
return;
}
char szResult[30];
SWORD nResult;
// require ODBC v2.0
AFX_SQL_SYNC(::SQLGetInfo(m_pDatabase->m_hdbc, SQL_ODBC_VER,
&szResult, _countof(szResult), &nResult));
if (!Check(nRetCode))
{
TRACE0("Error: ODBC failure checking for driver capabilities.\n");
ThrowDBException(nRetCode);
}
if (szResult[0] == '0' && szResult[1] < '2')
ThrowDBException(AFX_SQL_ERROR_ODBC_V2_REQUIRED);
}
// Check that driver supports requested cursor type
DWORD CRecordset::VerifyCursorSupport()
{
RETCODE nRetCode;
SWORD nResult;
UDWORD dwDriverScrollOptions;
AFX_SQL_SYNC(::SQLGetInfo(m_pDatabase->m_hdbc, SQL_SCROLL_OPTIONS,
&dwDriverScrollOptions, sizeof(dwDriverScrollOptions), &nResult));
if (!Check(nRetCode))
{
TRACE0("Error: ODBC failure checking for driver capabilities.\n");
ThrowDBException(nRetCode);
}
SDWORD dwScrollOptions = SQL_CURSOR_FORWARD_ONLY;
if (m_nOpenType == dynaset)
{
// Dynaset support requires ODBC's keyset driven cursor model
if (!(dwDriverScrollOptions & SQL_SO_KEYSET_DRIVEN))
ThrowDBException(AFX_SQL_ERROR_DYNASET_NOT_SUPPORTED);
dwScrollOptions = SQL_CURSOR_KEYSET_DRIVEN;
}
else if (m_nOpenType == snapshot)
{
// Snapshot support requires ODBC's static cursor model
if (!(dwDriverScrollOptions & SQL_SO_STATIC))
ThrowDBException(AFX_SQL_ERROR_SNAPSHOT_NOT_SUPPORTED);
dwScrollOptions = SQL_CURSOR_STATIC;
}
else
{
// Dynamic cursor support requires ODBC's dynamic cursor model
if (!(dwDriverScrollOptions & SQL_SO_DYNAMIC))
ThrowDBException(AFX_SQL_ERROR_DYNAMIC_CURSOR_NOT_SUPPORTED);
dwScrollOptions = SQL_CURSOR_DYNAMIC;
}
return dwScrollOptions;
}
void CRecordset::AllocAndCacheFieldInfo()
{
ASSERT(GetODBCFieldCount() < 0);
ASSERT(m_rgODBCFieldInfos == NULL);
RETCODE nRetCode;
SWORD nActualLen;
// Cache the number of result columns
AFX_ODBC_CALL(::SQLNumResultCols(m_hstmt, &m_nResultCols));
if (!Check(nRetCode))
{
TRACE0("Error: Can't get field info.\n");
ThrowDBException(nRetCode);
}
// If there are no fields quit now
if (m_nResultCols == 0)
return;
// Allocate buffer and get the ODBC meta data
m_rgODBCFieldInfos = new CODBCFieldInfo[m_nResultCols];
LPSTR lpszFieldName;
#ifdef _UNICODE
// Need proxy to temporarily store non-UNICODE name
lpszFieldName = new char[MAX_FNAME_LEN + 1];
#endif
// Get the field info each field
for (WORD n = 1; n <= GetODBCFieldCount(); n++)
{
#ifndef _UNICODE
// Reset the buffer to point to next element
lpszFieldName =
m_rgODBCFieldInfos[n - 1].m_strName.GetBuffer(MAX_FNAME_LEN + 1);
#endif
AFX_ODBC_CALL(::SQLDescribeCol(m_hstmt, n,
(UCHAR*)lpszFieldName, MAX_FNAME_LEN, &nActualLen,
&m_rgODBCFieldInfos[n - 1].m_nSQLType,
&m_rgODBCFieldInfos[n - 1].m_nPrecision,
&m_rgODBCFieldInfos[n - 1].m_nScale,
&m_rgODBCFieldInfos[n - 1].m_nNullability));
#ifndef _UNICODE
m_rgODBCFieldInfos[n - 1].m_strName.ReleaseBuffer(nActualLen);
#else
// Copy the proxy data to correct element
m_rgODBCFieldInfos[n - 1].m_strName = lpszFieldName;
#endif
if (!Check(nRetCode))
{
TRACE1("Error: ODBC failure getting field #%d info.\n", n);
ThrowDBException(nRetCode);
}
}
#ifdef _UNICODE
delete[] lpszFieldName;
#endif
}
void CRecordset::AllocRowset()
{
if (m_dwOptions & useMultiRowFetch)
SetRowsetSize(m_dwRowsetSize);
else
{
// Not using bulk row fetch, set rowset size to 1
m_rgRowStatus = new WORD[1];
m_dwRowsetSize = 1;
}
}
void CRecordset::FreeRowset()
{
// Delete the rowset status
delete [] m_rgRowStatus;
m_rgRowStatus = NULL;
if (m_dwOptions & useMultiRowFetch &&
!(m_dwOptions & userAllocMultiRowBuffers))
{
// Calling virtual function, DoBulkFieldExchange, here is bad
// because Close then FreeRowset may get called from destructor.
// There is no simple choice however if RFX_Bulk functions do
// a memory allocation. The net result is that users MUST call
// Close explicitly (rather than relying on destructor) if
// using multi row fetches, otherwise they will get a memory leak.
// If rowset already allocated, delete old rowset buffers
if (m_dwAllocatedRowsetSize != 0)
{
CFieldExchange fx(CFieldExchange::DeleteMultiRowBuffer, this);
DoBulkFieldExchange(&fx);
}
}
m_dwAllocatedRowsetSize = 0;
}
void CRecordset::EnableBookmarks()
{
// Turn on bookmark support if necessary
if (m_dwOptions & useBookmarks)
{
RETCODE nRetCode;
// Set stmt option if bookmarks supported by driver
if (m_pDatabase->GetBookmarkPersistence() & SQL_BP_SCROLL)
{
AFX_SQL_SYNC(::SQLSetStmtOption(m_hstmt, SQL_USE_BOOKMARKS,
SQL_UB_ON));
if (!Check(nRetCode))
{
TRACE0("Error: Can't enable bookmark support.\n");
ThrowDBException(nRetCode);
}
}
}
}
// Determine whether to use SQLSetPos or positioned update SQL
void CRecordset::SetUpdateMethod()
{
// Determine update method
if (m_pDatabase->m_dwUpdateOptions & AFX_SQL_SETPOSUPDATES)
m_bUseUpdateSQL = FALSE;
else if (m_pDatabase->m_dwUpdateOptions & AFX_SQL_POSITIONEDSQL)
m_bUseUpdateSQL = TRUE;
else
m_bUpdatable = FALSE;
}
// Determine which type of concurrency to set, set it and cursor type
void CRecordset::SetConcurrencyAndCursorType(HSTMT hstmt, DWORD dwScrollOptions)
{
RETCODE nRetCode;
SWORD nResult;
m_dwConcurrency = SQL_CONCUR_READ_ONLY;
if ((m_bUpdatable || m_bAppendable) && m_pDatabase->m_bUpdatable)
{
AFX_SQL_SYNC(::SQLGetInfo(m_pDatabase->m_hdbc, SQL_SCROLL_CONCURRENCY,
&m_dwDriverConcurrency, sizeof(m_dwDriverConcurrency), &nResult));
if (!Check(nRetCode))
{
TRACE0("Error: ODBC failure checking recordset updatability.\n");
ThrowDBException(nRetCode);
}
if (m_nLockMode == pessimistic)
{
if (m_dwDriverConcurrency & SQL_SCCO_LOCK)
m_dwConcurrency = SQL_CONCUR_LOCK;
#ifdef _DEBUG
else
if (afxTraceFlags & traceDatabase)
TRACE0("Warning: locking not supported, setting recordset read only.\n");
#endif
}
else
{
// Use cheapest, most concurrent model
if (m_dwDriverConcurrency & SQL_SCCO_OPT_ROWVER)
m_dwConcurrency = SQL_CONCUR_ROWVER;
else if (m_dwDriverConcurrency & SQL_SCCO_OPT_VALUES)
m_dwConcurrency = SQL_CONCUR_VALUES;
else if (m_dwDriverConcurrency & SQL_SCCO_LOCK)
m_dwConcurrency = SQL_CONCUR_LOCK;
}
}
// Set cursor type (Let rowset size default to 1).
AFX_SQL_SYNC(::SQLSetStmtOption(hstmt, SQL_CURSOR_TYPE, dwScrollOptions));
if (!Check(nRetCode))
{
TRACE0("Error: ODBC failure setting recordset cursor type.\n");
ThrowDBException(nRetCode);
}
// Set the concurrency model (NOTE: may have to reset concurrency later).
AFX_SQL_SYNC(::SQLSetStmtOption(hstmt, SQL_CONCURRENCY, m_dwConcurrency));
if (!Check(nRetCode))
{
TRACE0("Error: ODBC failure setting recordset concurrency.\n");
ThrowDBException(nRetCode);
}
}
// Is there a join, stored proc call, GROUP BY, UNION or missing FROM?
BOOL CRecordset::IsSQLUpdatable(LPCTSTR lpszSQL)
{
// Parse for query procedure call keyword or return param
if (!(_tcsnicmp(lpszSQL, _afxCall, lstrlen(_afxCall)-1) == 0 ||
_tcsnicmp(lpszSQL, _afxParamCall, lstrlen(_afxParamCall)-1) == 0))
// Assume this is a select query
return IsSelectQueryUpdatable(lpszSQL);
else
// Don't know the table name to update in procedure call
return FALSE;
}
BOOL CRecordset::IsSelectQueryUpdatable(LPCTSTR lpszSQL)
{
LPCTSTR lpchTokenFrom;
LPCTSTR lpchToken;
LPCTSTR lpchTokenNext;
LPTSTR lpszSQLStart;
CString strSQL = lpszSQL;
lpchTokenFrom = FindSQLToken(strSQL, _afxFrom);
if (lpchTokenFrom == NULL)
{
#ifdef _DEBUG
if (afxTraceFlags & traceDatabase)
TRACE0("Warning: Missing ' FROM ', recordset not updatable \n");
#endif
return FALSE;
}
lpchToken = FindSQLToken(strSQL, _T(" GROUP BY "));
if (lpchToken != NULL)
{
#ifdef _DEBUG
if (afxTraceFlags & traceDatabase)
TRACE0("Warning: SQL contains ' GROUP BY ', recordset not updatable \n");
#endif
return FALSE;
}
lpchToken = FindSQLToken(strSQL, _T(" UNION "));
if (lpchToken != NULL)
{
#ifdef _DEBUG
if (afxTraceFlags & traceDatabase)
TRACE0("Warning: SQL contains ' UNION ', recordset not updatable \n");
#endif
return FALSE;
}
// Find next token after FROM (can't have HAVING clause without GROUP BY)
lpchToken = FindSQLToken(strSQL, _afxWhere);
lpchTokenNext = FindSQLToken(strSQL, _afxOrderBy);
lpszSQLStart = strSQL.GetBuffer(0);
if (lpchTokenNext == NULL)
lpchTokenNext = lpchToken;
else if (lpchToken != NULL && lpchToken < lpchTokenNext)
lpchTokenNext = lpchToken;
if (lpchTokenNext != NULL)
{
int nFromLength = lpchTokenNext - lpchTokenFrom;
memcpy(lpszSQLStart, lpchTokenFrom, nFromLength*sizeof(TCHAR));
lpszSQLStart[nFromLength] = '\0';
}
else
lstrcpy(lpszSQLStart, lpchTokenFrom);
strSQL.ReleaseBuffer();
if (IsJoin(strSQL))
{
#ifdef _DEBUG
if (afxTraceFlags & traceDatabase)
TRACE0("Warning: SQL contains join, recordset not updatable \n");
#endif
return FALSE;
}
// Cache table name (skip over " FROM ")
m_strTableName = strSQL.Right(strSQL.GetLength()-6);
return TRUE;
}
// Check FROM clause for join syntax
BOOL PASCAL CRecordset::IsJoin(LPCTSTR lpszJoinClause)
{
// Look for comma in join clause
if (FindSQLToken(lpszJoinClause, _afxComma) != NULL)
return TRUE;
// Look for outer join clause
if (FindSQLToken(lpszJoinClause, _T(" JOIN ")) != NULL)
return TRUE;
return FALSE;
}
// Searches string for given token not in single quotes or brackets
LPCTSTR PASCAL CRecordset::FindSQLToken(LPCTSTR lpszSQL, LPCTSTR lpszSQLToken)
{
BOOL bInLiteral;
BOOL bInBrackets;
int nLeftBrackets;
int nRightBrackets;
LPCTSTR lpch;
LPCTSTR lpchSQLStart;
LPCTSTR lpszFoundToken;
int nTokenOffset = 0;
CString strSQL = lpszSQL;
strSQL.MakeUpper();
lpszFoundToken = strSQL.GetBuffer(0);
lpchSQLStart = lpszFoundToken;
do
{
lpszFoundToken = _tcsstr(lpszFoundToken + nTokenOffset, lpszSQLToken);
if (lpszFoundToken == NULL)
{
strSQL.ReleaseBuffer();
return NULL;
}
bInLiteral = bInBrackets = FALSE;
nLeftBrackets = nRightBrackets = 0;
// Check if embedded in literal or brackets
for (lpch = lpchSQLStart; lpch < lpszFoundToken; lpch = _tcsinc(lpch))
{
if (*lpch == _afxLiteralSeparator)
{
// Skip if escape literal
if (*_tcsinc(lpch) == _afxLiteralSeparator)
lpch = _tcsinc(lpch);
else
bInLiteral = !bInLiteral;
}
else if (!bInLiteral && (*lpch == '['))
{
// Skip if escape left bracket
if (*_tcsinc(lpch) == '[')
lpch = _tcsinc(lpch);
else
{
nLeftBrackets++;
if ((nLeftBrackets - nRightBrackets) > 0)
bInBrackets = TRUE;
else
bInBrackets = FALSE;
}
}
else if (!bInLiteral && (*lpch == ']'))
{
// Skip if escape right bracket
if (*_tcsinc(lpch) == ']')
lpch = _tcsinc(lpch);
else
{
nRightBrackets++;
if ((nLeftBrackets - nRightBrackets) > 0)
bInBrackets = TRUE;
else
bInBrackets = FALSE;
}
}
}
// If first iteration, reset the offset to jump over found token
if (nTokenOffset == 0)
nTokenOffset = lstrlen(lpszSQLToken);
} while (bInLiteral || bInBrackets);
lpszFoundToken = lpszSQL + (lpszFoundToken - lpchSQLStart);
strSQL.ReleaseBuffer();
return lpszFoundToken;
}
// Bind fields (if not already bound), then retrieve 1st record
void CRecordset::InitRecord()
{
// fields to bind
if (m_nFields != 0)
{
m_nFieldsBound = BindFieldsToColumns();
// m_nFields doesn't reflect number of
// RFX_ output column calls in Do[Bulk]FieldExchange
ASSERT((int)m_nFields == m_nFieldsBound);
// Allocate the data cache if necessary
if (m_nFields > 0 && m_bCheckCacheForDirtyFields)
AllocDataCache();
}
else
// No fields to bind, don't attempt to bind again
m_nFieldsBound = -1;
}
void CRecordset::ResetCursor()
{
m_bEOFSeen = m_bBOF = m_bEOF = FALSE;
m_bDeleted = FALSE;
m_lCurrentRecord = AFX_CURRENT_RECORD_BOF;
m_lRecordCount = 0;
}
void CRecordset::CheckRowsetCurrencyStatus(UWORD wFetchType, long nRows)
{
if (!m_bScrollable && wFetchType != SQL_FETCH_NEXT)
{
TRACE0("Error: forward-only recordsets only support MoveNext.\n");
ThrowDBException(AFX_SQL_ERROR_RECORDSET_FORWARD_ONLY);
}
if (IsEOF() && IsBOF())
{
// Can't position cursor if recordset empty
TRACE0("Error: attempted to position cursor on empty recordset.\n");
ThrowDBException(AFX_SQL_ERROR_NO_DATA_FOUND);
}
if (m_nOpenType != dynamic)
{
if (IsEOF() && (wFetchType == SQL_FETCH_NEXT ||
(wFetchType == SQL_FETCH_RELATIVE && nRows > 0)))
{
// if already at EOF, throw an exception
TRACE0("Error: attempted to move past EOF.\n");
ThrowDBException(AFX_SQL_ERROR_NO_DATA_FOUND);
}
else if (IsBOF() && (wFetchType == SQL_FETCH_PRIOR ||
(wFetchType == SQL_FETCH_RELATIVE && nRows < 0)))
{
// if already at BOF, throw an exception
TRACE0("Error: attempted to move before BOF.\n");
ThrowDBException(AFX_SQL_ERROR_NO_DATA_FOUND);
}
}
}
RETCODE CRecordset::FetchData(UWORD wFetchType, SDWORD nRow,
DWORD* pdwRowsFetched)
{
RETCODE nRetCode;
if (m_nOpenType == forwardOnly && !(m_dwOptions & useExtendedFetch))
{
ASSERT(wFetchType == SQL_FETCH_NEXT);
AFX_ODBC_CALL(::SQLFetch(m_hstmt));
*pdwRowsFetched = 1;
m_bDeleted = FALSE;
}
else
{
AFX_ODBC_CALL(::SQLExtendedFetch(m_hstmt, wFetchType,
nRow, pdwRowsFetched, m_rgRowStatus));
// Set deleted status
m_bDeleted = GetRowStatus(1) == SQL_ROW_DELETED;
}
CheckRowsetError(nRetCode);
return nRetCode;
}
void CRecordset::SkipDeletedRecords(UWORD wFetchType, long nRows,
DWORD* pdwRowsFetched, RETCODE* pnRetCode)
{
ASSERT(!(m_dwOptions & useMultiRowFetch));
ASSERT(wFetchType == SQL_FETCH_RELATIVE ||
wFetchType == SQL_FETCH_FIRST ||
wFetchType == SQL_FETCH_NEXT ||
wFetchType == SQL_FETCH_LAST ||
wFetchType == SQL_FETCH_PRIOR);
ASSERT(nRows != 0);
UWORD wDeletedFetchType = wFetchType;
DWORD dwDeletedRows = abs(nRows);
BOOL m_bDone;
switch (wFetchType)
{
case SQL_FETCH_FIRST:
wDeletedFetchType = SQL_FETCH_NEXT;
break;
case SQL_FETCH_LAST:
wDeletedFetchType = SQL_FETCH_PRIOR;
break;
case SQL_FETCH_RELATIVE:
if (nRows > 0)
wDeletedFetchType = SQL_FETCH_NEXT;
else
wDeletedFetchType = SQL_FETCH_PRIOR;
break;
}
// First fetch is as expected unless relative fetch
if (wFetchType != SQL_FETCH_RELATIVE)
{
*pnRetCode = FetchData(wFetchType, 1, pdwRowsFetched);
m_bDone = !m_bDeleted;
}
else
{
// Since deleted records must be skipped Move(n)
// must be turned into n MoveNext/MovePrev calls
*pnRetCode = FetchData(wDeletedFetchType, 1, pdwRowsFetched);
if (!m_bDeleted)
{
dwDeletedRows--;
m_bDone = dwDeletedRows == 0;
}
else
m_bDone = FALSE;
}
// Continue fetching until all req'd deleted records skipped
while (*pnRetCode != SQL_NO_DATA_FOUND && !m_bDone)
{
*pnRetCode = FetchData(wDeletedFetchType, 1, pdwRowsFetched);
if (wFetchType == SQL_FETCH_RELATIVE)
{
if (!m_bDeleted)
{
dwDeletedRows--;
m_bDone = dwDeletedRows == 0;
}
else
m_bDone = FALSE;
}
else
m_bDone = !m_bDeleted;
}
}
void CRecordset::SetRowsetCurrencyStatus(RETCODE nRetCode,
UWORD wFetchType, long nRows, DWORD dwRowsFetched)
{
UNUSED_ALWAYS(dwRowsFetched);
// Set the fetch direction
int nDirection = 0;
switch (wFetchType)
{
case SQL_FETCH_FIRST:
nDirection = 1;
if (nRetCode == SQL_NO_DATA_FOUND)
{
m_lCurrentRecord = AFX_CURRENT_RECORD_UNDEFINED;
m_lRecordCount = 0;
}
else
m_lCurrentRecord = 0;
break;
case SQL_FETCH_NEXT:
nDirection = 1;
AfxSetCurrentRecord(&m_lCurrentRecord, nRows, nRetCode);
AfxSetRecordCount(&m_lRecordCount, m_lCurrentRecord,
m_bEOFSeen, nRetCode);
// This is the only way to know you've hit the end (m_bEOFSeen)
if (!m_bEOFSeen && nRetCode == SQL_NO_DATA_FOUND &&
m_lRecordCount == m_lCurrentRecord + 1)
{
m_bEOFSeen = TRUE;
}
break;
case SQL_FETCH_LAST:
nDirection = -1;
if (nRetCode == SQL_NO_DATA_FOUND)
{
m_lCurrentRecord = AFX_CURRENT_RECORD_UNDEFINED;
m_lRecordCount = 0;
}
else if (m_bEOFSeen)
m_lCurrentRecord = m_lRecordCount - 1;
else
m_lCurrentRecord = AFX_CURRENT_RECORD_UNDEFINED;
break;
case SQL_FETCH_PRIOR:
nDirection = -1;
// If doing MovePrev after m_bEOF, don't increment current rec
if (!m_bEOF)
AfxSetCurrentRecord(&m_lCurrentRecord, nRows, nRetCode);
break;
case SQL_FETCH_RELATIVE:
nDirection = nRows;
AfxSetCurrentRecord(&m_lCurrentRecord, nRows, nRetCode);
AfxSetRecordCount(&m_lRecordCount, m_lCurrentRecord,
m_bEOFSeen, nRetCode);
break;
case SQL_FETCH_ABSOLUTE:
nDirection = nRows;
if (nRetCode != SQL_NO_DATA_FOUND)
{
if (nRows > 0)
m_lCurrentRecord = nRows - 1;
else if (m_bEOFSeen)
m_lCurrentRecord = m_lRecordCount + nRows;
else
m_lCurrentRecord = AFX_CURRENT_RECORD_UNDEFINED;
}
else
m_lCurrentRecord = AFX_CURRENT_RECORD_UNDEFINED;
AfxSetRecordCount(&m_lRecordCount, m_lCurrentRecord,
m_bEOFSeen, nRetCode);
break;
case SQL_FETCH_BOOKMARK:
nDirection = 0;
m_lCurrentRecord = AFX_CURRENT_RECORD_UNDEFINED;
break;
}
// Set the BOF/EOF flags
if (nRetCode == SQL_NO_DATA_FOUND)
{
if (wFetchType == SQL_FETCH_FIRST || wFetchType == SQL_FETCH_LAST ||
wFetchType == SQL_FETCH_BOOKMARK)
{
// If MoveFirst/MoveLast fails, result set is empty
// If SetBookmark fails, currency undefined
m_bEOF = m_bBOF = TRUE;
}
else
{
m_bEOF = nDirection >= 0;
m_bBOF = !m_bEOF;
}
}
else
{
m_bEOF = m_bBOF = FALSE;
}
}
void CRecordset::RefreshRowset(WORD wRow, WORD wLockType)
{
ASSERT(IsOpen());
ASSERT(m_dwOptions & useMultiRowFetch);
RETCODE nRetCode;
AFX_ODBC_CALL(::SQLSetPos(m_hstmt, wRow, SQL_REFRESH, wLockType));
// Need to fixup bound fields in some cases
if (m_nFields > 0 && !IsEOF() && !IsBOF() &&
!(m_dwOptions & useMultiRowFetch))
{
Fixups();
}
}
void CRecordset::SetRowsetCursorPosition(WORD wRow, WORD wLockType)
{
ASSERT(IsOpen());
ASSERT(m_dwOptions & useMultiRowFetch);
RETCODE nRetCode;
AFX_ODBC_CALL(::SQLSetPos(m_hstmt, wRow, SQL_POSITION, wLockType));
}
// "SELECT <user column name list> FROM <table name>"
void CRecordset::BuildSelectSQL()
{
ASSERT_VALID(this);
ASSERT(m_hstmt != SQL_NULL_HSTMT);
// Ignore queries with procedure call keyword or output param
if (!(_tcsnicmp(m_strSQL, _afxCall, lstrlen(_afxCall)-1) == 0 ||
_tcsnicmp(m_strSQL, _afxParamCall, lstrlen(_afxParamCall)-1) == 0))
{
// Ignore queries already built
if (_tcsnicmp(m_strSQL, _afxSelect, lstrlen(_afxSelect)-1) != 0)
{
// Assume m_strSQL specifies table name
ASSERT(m_nFields != 0);
CString strTableName;
strTableName = m_strSQL;
m_strSQL.Empty();
m_strSQL = _afxSelect;
// Set all fields dirty. AppendNames only outputs dirty field names
SetFieldDirty(NULL);
if (AppendNames(&m_strSQL, _T(",")) == 0)
{
TRACE0("Error: no field names - at least 1 required.\n");
ThrowDBException(AFX_SQL_ERROR_EMPTY_COLUMN_LIST);
}
// Overwrite final ',' separator with ' '
ASSERT(m_strSQL[m_strSQL.GetLength()-1] == ',');
m_strSQL.SetAt(m_strSQL.GetLength()-1, ' ');
m_strSQL += _afxFrom;
m_strSQL += strTableName;
}
}
}
// Add the filter and sort strings to query SQL
void CRecordset::AppendFilterAndSortSQL()
{
if (!m_strFilter.IsEmpty())
{
m_strSQL += _afxWhere;
m_strSQL += m_strFilter;
}
if (!m_strSort.IsEmpty())
{
m_strSQL += _afxOrderBy;
m_strSQL += m_strSort;
}
}
// Check for required SQLGetData support and do limited SQL parsing
BOOL CRecordset::IsRecordsetUpdatable()
{
// Do limited SQL parsing to determine if SQL updatable
if (!IsSQLUpdatable(m_strSQL))
return FALSE;
// Updatable recordsets with long binary columns must support
// SQL_GD_BOUND to use SQLSetPos, otherwise must use SQL updates
BOOL bUpdatable = TRUE;
if (m_bLongBinaryColumns && !m_bUseUpdateSQL)
{
// Set non-updatable if you can't use SQLGetData on bound columns
if (!(m_pDatabase->m_dwUpdateOptions & AFX_SQL_GDBOUND))
{
// Okay can't use SetPos, try and use positioned update SQL
if (m_pDatabase->m_dwUpdateOptions & AFX_SQL_POSITIONEDSQL)
{
m_bUseUpdateSQL = TRUE;
#ifdef _DEBUG
if (afxTraceFlags & traceDatabase)
{
TRACE0("Warning: Can't use SQLSetPos due to lack of SQLGetData support.\n");
TRACE0("\tWill use positioned update SQL.\n");
}
#endif
}
else
{
#ifdef _DEBUG
if (afxTraceFlags & traceDatabase)
TRACE0("Warning: Setting recordset read only due to lack of SQLGetData support.\n");
#endif
bUpdatable = FALSE;
}
}
}
return bUpdatable;
}
// Execute the update (or delete) using SQLSetPos
void CRecordset::ExecuteSetPosUpdate()
{
UWORD wExpectedRowStatus;
UWORD wPosOption;
if (m_nEditMode == noMode)
{
wPosOption = SQL_DELETE;
wExpectedRowStatus = SQL_ROW_DELETED;
}
else
{
if (m_nEditMode == edit)
{
wPosOption = SQL_UPDATE;
wExpectedRowStatus = SQL_ROW_UPDATED;
}
else
{
wPosOption = SQL_ADD;
wExpectedRowStatus = SQL_ROW_ADDED;
}
}
BindFieldsForUpdate();
RETCODE nRetCode;
AFX_ODBC_CALL(::SQLSetPos(m_hstmt, 1, wPosOption, SQL_LOCK_NO_CHANGE));
if (!Check(nRetCode))
{
TRACE0("Error: failure updating record.\n");
AfxThrowDBException(nRetCode, m_pDatabase, m_hstmt);
}
// Only have data-at-execution columns for CLongBinary columns
if (nRetCode == SQL_NEED_DATA)
SendLongBinaryData(m_hstmt);
// This should only fail if SQLSetPos returned SQL_SUCCESS_WITH_INFO explaining why
if (nRetCode == SQL_SUCCESS_WITH_INFO && GetRowStatus(1) != wExpectedRowStatus)
ThrowDBException(AFX_SQL_ERROR_UPDATE_DELETE_FAILED);
UnbindFieldsForUpdate();
}
// Prepare for sending update SQL by initializing m_hstmtUpdate
void CRecordset::PrepareUpdateHstmt()
{
RETCODE nRetCode;
if (m_hstmtUpdate == SQL_NULL_HSTMT)
{
AFX_SQL_SYNC(::SQLAllocStmt(m_pDatabase->m_hdbc, &m_hstmtUpdate));
if (!Check(nRetCode))
{
TRACE0("Error: failure to allocate update statement.\n");
AfxThrowDBException(nRetCode, m_pDatabase, m_hstmtUpdate);
}
}
else
{
AFX_SQL_SYNC(::SQLFreeStmt(m_hstmtUpdate, SQL_CLOSE));
if (!Check(nRetCode))
goto LErrRetCode;
// Re-use (prepared) hstmt & param binding if optimizeBulkAdd option
if(!(m_dwOptions & optimizeBulkAdd))
{
AFX_SQL_SYNC(::SQLFreeStmt(m_hstmtUpdate, SQL_RESET_PARAMS));
if (!Check(nRetCode))
{
LErrRetCode:
// Bad hstmt, free it and allocate another one
AFX_SQL_SYNC(::SQLFreeStmt(m_hstmtUpdate, SQL_DROP));
m_hstmtUpdate = SQL_NULL_HSTMT;
AFX_SQL_SYNC(::SQLAllocStmt(m_pDatabase->m_hdbc, &m_hstmtUpdate));
if (!Check(nRetCode))
{
TRACE0("Error: failure to allocate update statement.\n");
AfxThrowDBException(nRetCode, m_pDatabase, m_hstmtUpdate);
}
}
}
}
}
// Build the UPDATE, INSERT or DELETE SQL
void CRecordset::BuildUpdateSQL()
{
switch (m_nEditMode)
{
case noMode:
// DELETE FROM <tablename> WHERE CURRENT OF
{
m_strUpdateSQL = _T("DELETE FROM ");
m_strUpdateSQL += m_strTableName;
}
break;
case addnew:
// INSERT INTO <tablename> (<colname1>[,<colname2>]) VALUES (?[,?])
{
m_strUpdateSQL = _T("INSERT INTO ");
m_strUpdateSQL += m_strTableName;
m_strUpdateSQL += _T(" (");
// Append column names
AppendNames(&m_strUpdateSQL, _afxComma);
// overwrite last ',' with ')'
ASSERT(m_strUpdateSQL[m_strUpdateSQL.GetLength()-1] == ',');
m_strUpdateSQL.SetAt(m_strUpdateSQL.GetLength()-1, ')');
// Append values
m_strUpdateSQL += _T(" VALUES (");
AppendValues(m_hstmtUpdate, &m_strUpdateSQL, _afxComma);
// overwrite last ',' with ')'
ASSERT(m_strUpdateSQL[m_strUpdateSQL.GetLength()-1] == ',');
m_strUpdateSQL.SetAt(m_strUpdateSQL.GetLength()-1, ')');
}
break;
case edit:
// UPDATE <tablename> SET <colname1>=?[,<colname2>=?] WHERE CURRENT OF
{
m_strUpdateSQL = _T("UPDATE ");
m_strUpdateSQL += m_strTableName;
m_strUpdateSQL += _T(" SET ");
AppendNamesValues(m_hstmtUpdate, &m_strUpdateSQL, _afxComma);
// overwrite last ',' with ' '
ASSERT(m_strUpdateSQL[m_strUpdateSQL.GetLength()-1] == ',');
m_strUpdateSQL.SetAt(m_strUpdateSQL.GetLength()-1, ' ');
}
break;
}
// Update and Delete need "WHERE CURRENT OF <cursorname>"
if (m_nEditMode == edit || m_nEditMode == noMode)
{
m_strUpdateSQL += _T(" WHERE CURRENT OF ");
// Cache cursor name assigned by ODBC
if (m_strCursorName.IsEmpty())
{
// Get predefined cursor name from datasource
RETCODE nRetCode;
UCHAR szCursorName[MAX_CURSOR_NAME+1];
SWORD nLength = _countof(szCursorName)-1;
AFX_SQL_SYNC(::SQLGetCursorName(m_hstmt,
szCursorName, _countof(szCursorName), &nLength));
if (!Check(nRetCode))
ThrowDBException(nRetCode);
m_strCursorName = (char*)szCursorName;
}
m_strUpdateSQL += m_strCursorName;
}
m_pDatabase->ReplaceBrackets(m_strUpdateSQL.GetBuffer(0));
m_strUpdateSQL.ReleaseBuffer();
// Must prepare the hstmt on first optimized bulk add
if(m_dwOptions & firstBulkAdd)
{
RETCODE nRetCode;
USES_CONVERSION;
AFX_ODBC_CALL(::SQLPrepare(m_hstmtUpdate,
(UCHAR*)T2A((LPTSTR)(LPCTSTR)m_strUpdateSQL), SQL_NTS));
if (!Check(nRetCode))
ThrowDBException(nRetCode, m_hstmtUpdate);
}
}
void CRecordset::ExecuteUpdateSQL()
{
RETCODE nRetCode;
if(!(m_dwOptions & optimizeBulkAdd))
{
USES_CONVERSION;
AFX_ODBC_CALL(::SQLExecDirect(m_hstmtUpdate,
(UCHAR*)T2A((LPTSTR)(LPCTSTR)m_strUpdateSQL), SQL_NTS));
if (!Check(nRetCode))
ThrowDBException(nRetCode, m_hstmtUpdate);
}
else
{
AFX_ODBC_CALL(::SQLExecute(m_hstmtUpdate));
if (!Check(nRetCode))
ThrowDBException(nRetCode, m_hstmtUpdate);
}
// Only have data-at-execution parameters for CLongBinary columns
if (nRetCode == SQL_NEED_DATA)
SendLongBinaryData(m_hstmtUpdate);
SDWORD lRowsAffected = 0;
AFX_SQL_SYNC(::SQLRowCount(m_hstmtUpdate, &lRowsAffected));
if (!Check(nRetCode) || lRowsAffected == -1)
{
// Assume 1 row affected if # rows affected can't be determined
lRowsAffected = 1;
}
else
{
if (lRowsAffected != 1)
{
#ifdef _DEBUG
if (afxTraceFlags & traceDatabase)
TRACE1("Warning: %u rows affected by update operation (expected 1).\n",
lRowsAffected);
#endif
ThrowDBException((RETCODE)(lRowsAffected == 0 ?
AFX_SQL_ERROR_NO_ROWS_AFFECTED :
AFX_SQL_ERROR_MULTIPLE_ROWS_AFFECTED));
}
}
m_strUpdateSQL.Empty();
}
void CRecordset::SendLongBinaryData(HSTMT hstmt)
{
RETCODE nRetCode;
void* pv;
AFX_ODBC_CALL(::SQLParamData(hstmt, &pv));
if (!Check(nRetCode))
{
// cache away error
CDBException* pException = new CDBException(nRetCode);
pException->BuildErrorString(m_pDatabase, hstmt);
// then cancel Execute operation
Cancel();
THROW(pException);
}
while (nRetCode == SQL_NEED_DATA)
{
CLongBinary* pLongBinary = (CLongBinary*)pv;
ASSERT_VALID(pLongBinary);
const BYTE* lpData = (const BYTE*)::GlobalLock(pLongBinary->m_hData);
ASSERT(lpData != NULL);
AFX_ODBC_CALL(::SQLPutData(hstmt, (PTR)lpData,
pLongBinary->m_dwDataLength));
::GlobalUnlock(pLongBinary->m_hData);
if (!Check(nRetCode))
{
// cache away error
CDBException* pException = new CDBException(nRetCode);
pException->BuildErrorString(m_pDatabase, hstmt);
// then cancel Execute operation
Cancel();
THROW(pException);
}
// Check for another DATA_AT_EXEC
AFX_ODBC_CALL(::SQLParamData(hstmt, &pv));
if (!Check(nRetCode))
{
TRACE0("Error: failure handling long binary value during update.\n");
ThrowDBException(nRetCode, hstmt);
}
}
}
//////////////////////////////////////////////////////////////////////////////
// CRecordset RFX implementations
void CRecordset::AllocStatusArrays()
{
TRY
{
if (m_nFields != 0)
{
// Allocate buffers to hold field info
if (m_rgFieldInfos == NULL)
{
m_rgFieldInfos = new CFieldInfo[m_nFields];
memset(m_rgFieldInfos, 0, sizeof(CFieldInfo) * m_nFields);
}
if (m_pbFieldFlags == NULL)
{
m_pbFieldFlags = new BYTE[m_nFields];
memset(m_pbFieldFlags, 0, m_nFields);
}
}
if (m_nParams != 0)
{
// Allocate buffers to hold param info
if (m_pbParamFlags == NULL)
{
m_pbParamFlags = new BYTE[m_nParams];
memset(m_pbParamFlags, 0, m_nParams);
}
if (m_plParamLength == NULL)
{
m_plParamLength = new LONG[m_nParams];
memset(m_plParamLength, 0, m_nParams*sizeof(LONG));
}
}
}
CATCH_ALL(e)
{
Close();
THROW_LAST();
}
END_CATCH_ALL
}
int CRecordset::GetBoundFieldIndex(void* pv)
{
void* pvIndex;
if (!m_mapFieldIndex.Lookup(pv, pvIndex))
return -1;
else
// Cached value is short not ptr
return (int)pvIndex;
}
int CRecordset::GetBoundParamIndex(void* pv)
{
void* pvIndex;
if (!m_mapParamIndex.Lookup(pv, pvIndex))
return -1;
else
// Cached value in data not ptr
return (int)pvIndex;
}
short CRecordset::GetFieldIndexByName(LPCTSTR lpszFieldName)
{
for (short nIndex = 0; nIndex < GetODBCFieldCount(); nIndex++)
{
if (m_rgODBCFieldInfos[nIndex].m_strName == lpszFieldName)
break;
}
// Check if field name found
if (nIndex == GetODBCFieldCount())
ThrowDBException(AFX_SQL_ERROR_FIELD_NOT_FOUND);
return nIndex;
}
long* CRecordset::GetFieldLengthBuffer(DWORD nField, int nFieldType)
{
if (nFieldType == CFieldExchange::outputColumn)
{
ASSERT(nField < m_nFields);
return &m_rgFieldInfos[nField].m_nLength;
}
else
{
ASSERT(nField < m_nParams);
return &m_plParamLength[nField];
}
}
BYTE CRecordset::GetFieldStatus(DWORD nField)
{
ASSERT(nField < m_nFields);
return m_pbFieldFlags[nField];
}
void CRecordset::SetFieldStatus(DWORD nField, BYTE bFlags)
{
ASSERT(nField < m_nFields);
m_pbFieldFlags[nField] |= bFlags;
}
void CRecordset::ClearFieldStatus()
{
memset(m_pbFieldFlags, 0, m_nFields);
}
BOOL CRecordset::IsFieldStatusDirty(DWORD nField) const
{
ASSERT(nField < m_nFields);
return m_pbFieldFlags[nField] & AFX_SQL_FIELD_FLAG_DIRTY;
}
void CRecordset::SetDirtyFieldStatus(DWORD nField)
{
ASSERT(nField < m_nFields);
m_pbFieldFlags[nField] |= AFX_SQL_FIELD_FLAG_DIRTY;
}
void CRecordset::ClearDirtyFieldStatus(DWORD nField)
{
ASSERT(nField < m_nFields);
m_pbFieldFlags[nField] &= ~AFX_SQL_FIELD_FLAG_DIRTY;
}
BOOL CRecordset::IsFieldStatusNull(DWORD nField) const
{
ASSERT(nField < m_nFields);
return m_pbFieldFlags[nField] & AFX_SQL_FIELD_FLAG_NULL;
}
void CRecordset::SetNullFieldStatus(DWORD nField)
{
ASSERT(nField < m_nFields);
m_pbFieldFlags[nField] |= AFX_SQL_FIELD_FLAG_NULL;
}
void CRecordset::ClearNullFieldStatus(DWORD nField)
{
ASSERT(nField < m_nFields);
m_pbFieldFlags[nField] &= ~AFX_SQL_FIELD_FLAG_NULL;
}
BOOL CRecordset::IsParamStatusNull(DWORD nParam) const
{
ASSERT(nParam < m_nParams);
return m_pbParamFlags[nParam] & AFX_SQL_FIELD_FLAG_NULL;
}
void CRecordset::SetNullParamStatus(DWORD nParam)
{
ASSERT(nParam < m_nParams);
m_pbParamFlags[nParam] |= AFX_SQL_FIELD_FLAG_NULL;
}
void CRecordset::ClearNullParamStatus(DWORD nParam)
{
ASSERT(nParam < m_nParams);
m_pbParamFlags[nParam] &= ~AFX_SQL_FIELD_FLAG_NULL;
}
BOOL CRecordset::IsFieldNullable(DWORD nField) const
{
ASSERT(nField <= INT_MAX);
ASSERT((long)nField < GetODBCFieldCount());
// return TRUE if nulls allowed or if not known
return m_rgODBCFieldInfos[nField].m_nNullability != SQL_NO_NULLS;
}
UINT CRecordset::BindParams(HSTMT hstmt)
{
ASSERT_VALID(this);
ASSERT(m_hstmt != SQL_NULL_HSTMT);
CFieldExchange fx(CFieldExchange::BindParam, this);
fx.m_hstmt = hstmt;
DoFieldExchange(&fx);
return fx.m_nParams;
}
void CRecordset::RebindParams(HSTMT hstmt)
{
ASSERT_VALID(this);
ASSERT(m_hstmt != SQL_NULL_HSTMT);
if (m_bRebindParams)
{
CFieldExchange fx(CFieldExchange::RebindParam, this);
fx.m_hstmt = hstmt;
DoFieldExchange(&fx);
}
}
UINT CRecordset::BindFieldsToColumns()
{
ASSERT_VALID(this);
ASSERT(m_hstmt != SQL_NULL_HSTMT);
ASSERT(m_nFieldsBound == 0);
ASSERT(m_nFields != 0 && m_nFields <= 255);
CFieldExchange fx(CFieldExchange::BindFieldToColumn, this);
fx.m_hstmt = m_hstmt;
// Binding depends on fetch type
if (m_dwOptions & useMultiRowFetch)
DoBulkFieldExchange(&fx);
else
DoFieldExchange(&fx);
return fx.m_nFields;
}
void CRecordset::BindFieldsForUpdate()
{
ASSERT_VALID(this);
if (m_nEditMode == edit || m_nEditMode == addnew)
{
CFieldExchange fx(CFieldExchange::BindFieldForUpdate, this);
fx.m_hstmt = m_hstmt;
DoFieldExchange(&fx);
}
}
void CRecordset::UnbindFieldsForUpdate()
{
ASSERT_VALID(this);
if (m_nEditMode == edit || m_nEditMode == addnew)
{
CFieldExchange fx(CFieldExchange::UnbindFieldForUpdate, this);
fx.m_hstmt = m_hstmt;
DoFieldExchange(&fx);
}
}
// After Move operation, reflect status and lengths of columns in RFX fields
void CRecordset::Fixups()
{
ASSERT_VALID(this);
ASSERT(m_hstmt != SQL_NULL_HSTMT);
ASSERT(m_nFieldsBound != 0);
CFieldExchange fx(CFieldExchange::Fixup, this);
fx.m_hstmt = m_hstmt;
DoFieldExchange(&fx);
}
UINT CRecordset::AppendNames(CString* pstr, LPCTSTR lpszSeparator)
{
ASSERT_VALID(this);
ASSERT(AfxIsValidAddress(pstr, sizeof(CString)));
ASSERT(AfxIsValidString(lpszSeparator));
ASSERT(m_hstmt != SQL_NULL_HSTMT);
CFieldExchange fx(CFieldExchange::Name, this);
fx.m_pstr = pstr;
fx.m_lpszSeparator = lpszSeparator;
if (m_dwOptions & useMultiRowFetch)
DoBulkFieldExchange(&fx);
else
DoFieldExchange(&fx);
return fx.m_nFields;
}
// For each "changed" column, append <column name>=<column value>,
UINT CRecordset::AppendNamesValues(HSTMT hstmt, CString* pstr,
LPCTSTR lpszSeparator)
{
ASSERT_VALID(this);
ASSERT(AfxIsValidAddress(pstr, sizeof(CString)));
ASSERT(AfxIsValidString(lpszSeparator));
ASSERT(m_hstmt != SQL_NULL_HSTMT);
ASSERT(hstmt != SQL_NULL_HSTMT);
CFieldExchange fx(CFieldExchange::NameValue, this);
fx.m_pstr = pstr;
fx.m_lpszSeparator = lpszSeparator;
fx.m_hstmt = hstmt;
DoFieldExchange(&fx);
return fx.m_nFields;
}
// For each "changed" column, append <column value>,
UINT CRecordset::AppendValues(HSTMT hstmt, CString* pstr,
LPCTSTR lpszSeparator)
{
ASSERT_VALID(this);
ASSERT(AfxIsValidAddress(pstr, sizeof(CString)));
ASSERT(AfxIsValidString(lpszSeparator));
ASSERT(m_hstmt != SQL_NULL_HSTMT);
ASSERT(hstmt != SQL_NULL_HSTMT);
CFieldExchange fx(CFieldExchange::Value, this);
fx.m_pstr = pstr;
fx.m_lpszSeparator = lpszSeparator;
fx.m_hstmt = hstmt;
DoFieldExchange(&fx);
return fx.m_nFields;
}
// Cache fields of copy buffer in a CMemFile with CArchive
void CRecordset::StoreFields()
{
ASSERT_VALID(this);
ASSERT(m_nFieldsBound != 0);
CFieldExchange fx(CFieldExchange::StoreField, this);
DoFieldExchange(&fx);
}
// Restore fields of copy buffer from archived memory file
void CRecordset::LoadFields()
{
ASSERT_VALID(this);
ASSERT(m_nFieldsBound != 0);
// Must clear out the old status
ClearFieldStatus();
CFieldExchange fx(CFieldExchange::LoadField, this);
DoFieldExchange(&fx);
}
void CRecordset::MarkForUpdate()
{
ASSERT_VALID(this);
CFieldExchange fx(CFieldExchange::MarkForUpdate, this);
DoFieldExchange(&fx);
}
void CRecordset::MarkForAddNew()
{
ASSERT_VALID(this);
CFieldExchange fx(CFieldExchange::MarkForAddNew, this);
DoFieldExchange(&fx);
}
void CRecordset::AllocDataCache()
{
ASSERT_VALID(this);
CFieldExchange fx(CFieldExchange::AllocCache, this);
DoFieldExchange(&fx);
}
void CRecordset::FreeDataCache()
{
ASSERT_VALID(this);
CFieldInfo* pInfo;
for (DWORD nField = 0; nField < m_nFields; nField++)
{
pInfo = &m_rgFieldInfos[nField];
switch(pInfo->m_nDataType)
{
default:
ASSERT(FALSE);
// fall through
// Data not cached
case AFX_RFX_NO_TYPE:
break;
// Types cached by value (sizeof(TYPE) <= sizeof(void*))
case AFX_RFX_BOOL:
case AFX_RFX_BYTE:
case AFX_RFX_INT:
case AFX_RFX_LONG:
case AFX_RFX_SINGLE:
pInfo->m_pvDataCache = NULL;
break;
case AFX_RFX_TEXT:
delete (CString*)pInfo->m_pvDataCache;
pInfo->m_pvDataCache = NULL;
break;
case AFX_RFX_LPTSTR:
delete [] LPTSTR(pInfo->m_pvDataCache);
pInfo->m_pvDataCache = NULL;
break;
case AFX_RFX_DOUBLE:
delete (double*)pInfo->m_pvDataCache;
pInfo->m_pvDataCache = NULL;
break;
case AFX_RFX_TIMESTAMP:
delete (TIMESTAMP_STRUCT*)pInfo->m_pvDataCache;
pInfo->m_pvDataCache = NULL;
break;
case AFX_RFX_OLEDATE:
delete (COleDateTime*)pInfo->m_pvDataCache;
pInfo->m_pvDataCache = NULL;
break;
case AFX_RFX_DATE:
delete (CTime*)pInfo->m_pvDataCache;
pInfo->m_pvDataCache = NULL;
break;
case AFX_RFX_BINARY:
delete (CByteArray*)pInfo->m_pvDataCache;
pInfo->m_pvDataCache = NULL;
break;
}
}
}
#ifdef _DEBUG
void CRecordset::DumpFields(CDumpContext& dc) const
{
CFieldExchange fx(CFieldExchange::DumpField, (CRecordset *)this);
fx.m_pdcDump = &dc;
((CRecordset *)this)->DoFieldExchange(&fx);
}
#endif // _DEBUG
// Perform Update (m_nModeEdit == edit), Insert (addnew),
// or Delete (noMode)
BOOL CRecordset::UpdateInsertDelete()
{
ASSERT_VALID(this);
ASSERT(m_hstmt != SQL_NULL_HSTMT);
// Delete mode
if (m_nEditMode == addnew)
{
if (!m_bAppendable)
{
TRACE0("Error: attempted to add a record to a read only recordset.\n");
ThrowDBException(AFX_SQL_ERROR_RECORDSET_READONLY);
}
}
else
{
if (!m_bUpdatable)
{
TRACE0("Error: attempted to update a read only recordset.\n");
ThrowDBException(AFX_SQL_ERROR_RECORDSET_READONLY);
}
// Requires currency
if (m_bEOF || m_bBOF || m_bDeleted)
{
TRACE0("Error: attempting to update recordset - but no record is current.\n");
ThrowDBException(AFX_SQL_ERROR_NO_CURRENT_RECORD);
}
}
// Update or AddNew is NOP w/o at least 1 changed field
if (m_nEditMode != noMode && !IsFieldDirty(NULL))
return FALSE;
if (!m_bUseUpdateSQL)
{
// Most efficient update method
ExecuteSetPosUpdate();
}
else
{
BOOL bNullHstmt = (m_hstmtUpdate == NULL);
// Make sure m_hstmtUpdate allocated
PrepareUpdateHstmt();
// Build update SQL unless optimizing bulk adds and hstmt not NULL
if(!(m_dwOptions & optimizeBulkAdd) || bNullHstmt)
{
// Mark as first bulk add if optimizing
if(m_dwOptions & optimizeBulkAdd)
{
m_dwOptions &= ~optimizeBulkAdd;
m_dwOptions |= firstBulkAdd;
}
BuildUpdateSQL();
// Reset flag marking first optimization
if(m_dwOptions & firstBulkAdd)
{
m_dwOptions &= ~firstBulkAdd;
m_dwOptions |= optimizeBulkAdd;
}
}
else
{
// Just reset the data lengths and datetime proxies
AppendValues(m_hstmtUpdate, &m_strUpdateSQL, _afxComma);
}
ExecuteUpdateSQL();
}
TRY
{
// Delete
switch (m_nEditMode)
{
case noMode:
// Decrement record count
if (m_lCurrentRecord >= 0)
{
if (m_lRecordCount > 0)
m_lRecordCount--;
m_lCurrentRecord--;
}
// indicate on a deleted record
m_bDeleted = TRUE;
// Set all fields to NULL
SetFieldNull(NULL);
break;
case addnew:
// The recordset may no longer be empty (depending on driver behavior)
// reset m_bEOF/m_bBOF so Move can be called
m_bEOF = m_bBOF = FALSE;
if (m_pDatabase->m_bIncRecordCountOnAdd && m_lCurrentRecord >= 0)
{
if (m_lRecordCount != -1)
m_lRecordCount++;
m_lCurrentRecord++;
}
// Reset the data cache if necessary
if (m_bCheckCacheForDirtyFields && m_nFields > 0)
LoadFields();
break;
case edit:
break;
}
// Reset the edit mode
m_nEditMode = noMode;
}
END_TRY
// Unless doing a bulk AddNew, reset the dirty flags
if (m_bCheckCacheForDirtyFields && !(m_dwOptions & optimizeBulkAdd))
SetFieldDirty(NULL, FALSE);
// Must return TRUE since record updated
return TRUE;
}
// Fetch and alloc algorithms for CLongBinary data when length unknown
long CRecordset::GetLBFetchSize(long lOldSize)
{
// Make it twice as big
return lOldSize << 1;
}
long CRecordset::GetLBReallocSize(long lOldSize)
{
// Make it twice as big (no effect if less than fetch size)
return lOldSize << 1;
}
short PASCAL CRecordset::GetDefaultFieldType(short nSQLType)
{
short nFieldType = 0;
switch (nSQLType)
{
case SQL_BIT:
nFieldType = SQL_C_BIT;
break;
case SQL_TINYINT:
nFieldType = SQL_C_UTINYINT;
break;
case SQL_SMALLINT:
nFieldType = SQL_C_SSHORT;
break;
case SQL_INTEGER:
nFieldType = SQL_C_SLONG;
break;
case SQL_REAL:
nFieldType = SQL_C_FLOAT;
break;
case SQL_FLOAT:
case SQL_DOUBLE:
nFieldType = SQL_C_DOUBLE;
break;
case SQL_DATE:
case SQL_TIME:
case SQL_TIMESTAMP:
nFieldType = SQL_C_TIMESTAMP;
break;
case SQL_NUMERIC:
case SQL_DECIMAL:
case SQL_BIGINT:
case SQL_CHAR:
case SQL_VARCHAR:
case SQL_LONGVARCHAR:
nFieldType = SQL_C_CHAR;
break;
case SQL_BINARY:
case SQL_VARBINARY:
case SQL_LONGVARBINARY:
nFieldType = SQL_C_BINARY;
break;
default:
ASSERT(FALSE);
}
return nFieldType;
}
void* PASCAL CRecordset::GetDataBuffer(CDBVariant& varValue,
short nFieldType, int* pnLen, short nSQLType, UDWORD nPrecision)
{
void* pvData = NULL;
switch (nFieldType)
{
case SQL_C_BIT:
pvData = &varValue.m_boolVal;
varValue.m_dwType = DBVT_BOOL;
*pnLen = sizeof(varValue.m_boolVal);
break;
case SQL_C_UTINYINT:
pvData = &varValue.m_chVal;
varValue.m_dwType = DBVT_UCHAR;
*pnLen = sizeof(varValue.m_chVal);
break;
case SQL_C_SSHORT:
pvData = &varValue.m_iVal;
varValue.m_dwType = DBVT_SHORT;
*pnLen = sizeof(varValue.m_iVal);
break;
case SQL_C_SLONG:
pvData = &varValue.m_lVal;
varValue.m_dwType = DBVT_LONG;
*pnLen = sizeof(varValue.m_lVal);
break;
case SQL_C_FLOAT:
pvData = &varValue.m_fltVal;
varValue.m_dwType = DBVT_SINGLE;
*pnLen = sizeof(varValue.m_fltVal);
break;
case SQL_C_DOUBLE:
pvData = &varValue.m_dblVal;
varValue.m_dwType = DBVT_DOUBLE;
*pnLen = sizeof(varValue.m_dblVal);
break;
case SQL_C_TIMESTAMP:
pvData = varValue.m_pdate = new TIMESTAMP_STRUCT;
varValue.m_dwType = DBVT_DATE;
*pnLen = sizeof(*varValue.m_pdate);
break;
case SQL_C_CHAR:
varValue.m_pstring = new CString;
varValue.m_dwType = DBVT_STRING;
*pnLen = GetTextLen(nSQLType, nPrecision);
pvData = varValue.m_pstring->GetBufferSetLength(*pnLen);
break;
case SQL_C_BINARY:
varValue.m_pbinary = new CLongBinary;
varValue.m_dwType = DBVT_BINARY;
if (nSQLType == SQL_LONGVARBINARY)
{
// pvData can't be NULL, so nLen must be at least 1
*pnLen = 1;
}
else
{
// better know the length!
ASSERT(nPrecision != 0);
*pnLen = nPrecision;
}
varValue.m_pbinary->m_hData = ::GlobalAlloc(GMEM_MOVEABLE, *pnLen);
varValue.m_pbinary->m_dwDataLength = *pnLen;
pvData = ::GlobalLock(varValue.m_pbinary->m_hData);
break;
default:
ASSERT(FALSE);
}
return pvData;
}
int PASCAL CRecordset::GetTextLen(short nSQLType, UDWORD nPrecision)
{
int nLen;
if (nSQLType == SQL_LONGVARCHAR || nSQLType == SQL_LONGVARBINARY)
{
// Use a dummy length of 1 (will just get NULL terminator)
nLen = 1;
}
else
{
// better know the length
ASSERT(nPrecision >= 0);
nLen = nPrecision + 1;
// If converting Numeric or Decimal to text need
// room for decimal point and sign in string
if (nSQLType == SQL_NUMERIC || nSQLType == SQL_DECIMAL)
nLen += 2;
}
return nLen;
}
long PASCAL CRecordset::GetData(CDatabase* pdb, HSTMT hstmt,
short nFieldIndex, short nFieldType, LPVOID pvData, int nLen,
short nSQLType)
{
UNUSED(nSQLType);
long nActualSize;
RETCODE nRetCode;
// Retrieve the column in question
AFX_ODBC_CALL(::SQLGetData(hstmt, nFieldIndex,
nFieldType, pvData, nLen, &nActualSize));
// Ignore data truncated warnings for long data
if (nRetCode == SQL_SUCCESS_WITH_INFO)
{
#ifdef _DEBUG
CDBException e(nRetCode);
if (afxTraceFlags & traceDatabase)
{
CDBException e(nRetCode);
// Build the error string but don't send nuisance output to TRACE window
e.BuildErrorString(pdb, hstmt, FALSE);
// If not a data truncated warning on long var column,
// then send debug output
if ((nSQLType != SQL_LONGVARCHAR &&
nSQLType != SQL_LONGVARBINARY) ||
(e.m_strStateNativeOrigin.Find(_afxDataTruncated) < 0))
{
TRACE1("Warning: ODBC Success With Info on field %d.\n",
nFieldIndex - 1);
e.TraceErrorMessage(e.m_strError);
e.TraceErrorMessage(e.m_strStateNativeOrigin);
}
}
#endif // _DEBUG
}
else if (nRetCode == SQL_NO_DATA_FOUND)
{
TRACE0("Error: GetFieldValue operation failed on field %d.\n");
TRACE1("\tData already fetched for this field.\n",
nFieldIndex - 1);
AfxThrowDBException(nRetCode, pdb, hstmt);
}
else if (nRetCode != SQL_SUCCESS)
{
TRACE1("Error: GetFieldValue operation failed on field %d.\n",
nFieldIndex - 1);
AfxThrowDBException(nRetCode, pdb, hstmt);
}
return nActualSize;
}
void PASCAL CRecordset::GetLongCharDataAndCleanup(CDatabase* pdb,
HSTMT hstmt, short nFieldIndex, long nActualSize, LPVOID* ppvData,
int nLen, CString& strValue, short nSQLType)
{
RETCODE nRetCode;
// Release the buffer now that data has been fetched
strValue.ReleaseBuffer(nActualSize < nLen ? nActualSize : nLen);
// If long data, may need to call SQLGetData again
if (nLen <= nActualSize &&
(nSQLType == SQL_LONGVARCHAR || nSQLType == SQL_LONGVARBINARY))
{
// Reallocate the size (this will copy the data)
*ppvData = strValue.GetBufferSetLength(nActualSize + 1);
// Get pointer, skipping over original data, but not the NULL
int nOldLen = nLen - 1;
*ppvData = (BYTE*)*ppvData + nOldLen;
nLen = nActualSize + 1 - nOldLen;
// Retrieve the column in question
AFX_ODBC_CALL(::SQLGetData(hstmt, nFieldIndex,
SQL_C_CHAR, *ppvData, nLen, &nActualSize));
if (nRetCode == SQL_SUCCESS_WITH_INFO)
{
#ifdef _DEBUG
if (afxTraceFlags & traceDatabase)
{
TRACE1("Warning: ODBC Success With Info on field %d.\n",
nFieldIndex - 1);
CDBException e(nRetCode);
e.BuildErrorString(pdb, hstmt);
}
#endif // _DEBUG
}
else if (nRetCode != SQL_SUCCESS)
{
TRACE1("Error: GetFieldValue operation failed on field %d.\n",
nFieldIndex - 1);
AfxThrowDBException(nRetCode, pdb, hstmt);
}
// Release the buffer now that data has been fetched
strValue.ReleaseBuffer(nActualSize + nOldLen);
}
}
void PASCAL CRecordset::GetLongBinaryDataAndCleanup(CDatabase* pdb, HSTMT hstmt,
short nFieldIndex, long nActualSize, LPVOID* ppvData, int nLen,
CDBVariant& varValue, short nSQLType)
{
RETCODE nRetCode;
::GlobalUnlock(varValue.m_pbinary->m_hData);
// If long data, may need to call SQLGetData again
if (nLen < nActualSize && nSQLType == SQL_LONGVARBINARY)
{
// Reallocate a bigger buffer
HGLOBAL hOldData = varValue.m_pbinary->m_hData;
varValue.m_pbinary->m_hData = ::GlobalReAlloc(hOldData,
nActualSize, GMEM_MOVEABLE);
// Validate the memory was allocated and can be locked
if (varValue.m_pbinary->m_hData == NULL)
{
// Restore the old handle (not NULL if Realloc failed)
varValue.m_pbinary->m_hData = hOldData;
AfxThrowMemoryException();
}
varValue.m_pbinary->m_dwDataLength = nActualSize;
// Get pointer, skipping over original data
*ppvData = (BYTE*)::GlobalLock(varValue.m_pbinary->m_hData) + nLen;
int nOldLen = nLen;
nLen = nActualSize - nOldLen;
// Retrieve the column in question
AFX_ODBC_CALL(::SQLGetData(hstmt, nFieldIndex,
SQL_C_BINARY, *ppvData, nLen, &nActualSize));
if (nRetCode == SQL_SUCCESS_WITH_INFO)
{
#ifdef _DEBUG
if (afxTraceFlags & traceDatabase)
{
TRACE1("Warning: ODBC Success With Info on field %d.\n",
nFieldIndex - 1);
CDBException e(nRetCode);
e.BuildErrorString(pdb, hstmt);
}
#endif // _DEBUG
}
else if (nRetCode != SQL_SUCCESS)
{
TRACE1("Error: GetFieldValue operation failed on field %d.\n",
nFieldIndex - 1);
AfxThrowDBException(nRetCode, pdb, hstmt);
}
ASSERT((int)varValue.m_pbinary->m_dwDataLength ==
nActualSize + nOldLen);
// Release the buffer now that data has been fetched
::GlobalUnlock(varValue.m_pbinary->m_hData);
}
}
//////////////////////////////////////////////////////////////////////////////
// CRecordset diagnostics
#ifdef _DEBUG
void CRecordset::AssertValid() const
{
CObject::AssertValid();
if (m_pDatabase != NULL)
m_pDatabase->AssertValid();
}
void CRecordset::Dump(CDumpContext& dc) const
{
CObject::Dump(dc);
dc << "m_nOpenType = " << m_nOpenType;
dc << "\nm_strSQL = " << m_strSQL;
dc << "\nm_hstmt = " << m_hstmt;
dc << "\nm_bRecordsetDb = " << m_bRecordsetDb;
dc << "\nm_lOpen = " << m_lOpen;
dc << "\nm_bScrollable = " << m_bScrollable;
dc << "\nm_bUpdatable = " << m_bUpdatable;
dc << "\nm_bAppendable = " << m_bAppendable;
dc << "\nm_nFields = " << m_nFields;
dc << "\nm_nFieldsBound = " << m_nFieldsBound;
dc << "\nm_nParams = " << m_nParams;
dc << "\nm_bEOF = " << m_bEOF;
dc << "\nm_bBOF = " << m_bBOF;
dc << "\nm_bDeleted = " << m_bEOF;
dc << "\nm_bLockMode = " << m_nLockMode;
dc << "\nm_nEditMode = " << m_nEditMode;
dc << "\nm_strCursorName = " << m_strCursorName;
dc << "\nm_hstmtUpdate = " << m_hstmtUpdate;
dc << "\nDump values for each field in current record.";
DumpFields(dc);
if (dc.GetDepth() > 0)
{
if (m_pDatabase == NULL)
dc << "with no database\n";
else
dc << "with database: " << m_pDatabase;
}
}
#endif // _DEBUG
//////////////////////////////////////////////////////////////////////////////
// Helpers
void AFXAPI AfxSetCurrentRecord(long* plCurrentRecord, long nRows, RETCODE nRetCode)
{
if (*plCurrentRecord != AFX_CURRENT_RECORD_UNDEFINED &&
nRetCode != SQL_NO_DATA_FOUND)
*plCurrentRecord += nRows;
}
void AFXAPI AfxSetRecordCount(long* plRecordCount, long lCurrentRecord,
long nRows, BOOL bEOFSeen, RETCODE nRetCode)
{
// This function provided for backward binary compatibility
UNUSED(nRows); // not used in release build
ASSERT(nRows == 1);
AfxSetRecordCount(plRecordCount, lCurrentRecord, bEOFSeen, nRetCode);
}
void AFXAPI AfxSetRecordCount(long* plRecordCount, long lCurrentRecord,
BOOL bEOFSeen, RETCODE nRetCode)
{
// If not at the end and haven't yet been to the end, incr count
if (nRetCode != SQL_NO_DATA_FOUND && !bEOFSeen &&
lCurrentRecord != AFX_CURRENT_RECORD_UNDEFINED)
{
// lCurrentRecord 0-based and it's already been set
*plRecordCount =
__max(*plRecordCount, lCurrentRecord + 1);
}
}
//////////////////////////////////////////////////////////////////////////////
// Inline function declarations expanded out-of-line
#ifndef _AFX_ENABLE_INLINES
static char _szAfxDbInl[] = "afxdb.inl";
#undef THIS_FILE
#define THIS_FILE _szAfxDbInl
#define _AFXDBCORE_INLINE
#include "afxdb.inl"
#endif
#ifdef AFX_INIT_SEG
#pragma code_seg(AFX_INIT_SEG)
#endif
IMPLEMENT_DYNAMIC(CDBException, CException)
IMPLEMENT_DYNAMIC(CDatabase, CObject)
IMPLEMENT_DYNAMIC(CRecordset, CObject)
#pragma warning(disable: 4074)
#pragma init_seg(lib)
PROCESS_LOCAL(_AFX_DB_STATE, _afxDbState)
/////////////////////////////////////////////////////////////////////////////