// 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 FROM " 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 WHERE CURRENT OF { m_strUpdateSQL = _T("DELETE FROM "); m_strUpdateSQL += m_strTableName; } break; case addnew: // INSERT INTO ([,]) 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 SET =?[,=?] 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 " 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 =, 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 , 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) /////////////////////////////////////////////////////////////////////////////