@@ -3220,6 +3220,20 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum
32203220
32213221 std::string decimalSeparator = GetDecimalSeparator (); // Cache decimal separator
32223222
3223+ // OPTIMIZATION #3: Prefetch column metadata into cache-friendly arrays
3224+ // Eliminates repeated struct field access (O(rows × cols)) in the hot loop below
3225+ std::vector<SQLSMALLINT> dataTypes (numCols);
3226+ std::vector<SQLULEN> columnSizes (numCols);
3227+ std::vector<uint64_t > fetchBufferSizes (numCols);
3228+ std::vector<bool > isLobs (numCols);
3229+
3230+ for (SQLUSMALLINT col = 0 ; col < numCols; col++) {
3231+ dataTypes[col] = columnInfos[col].dataType ;
3232+ columnSizes[col] = columnInfos[col].processedColumnSize ;
3233+ fetchBufferSizes[col] = columnInfos[col].fetchBufferSize ;
3234+ isLobs[col] = columnInfos[col].isLob ;
3235+ }
3236+
32233237 size_t initialSize = rows.size ();
32243238 for (SQLULEN i = 0 ; i < numRowsFetched; i++) {
32253239 rows.append (py::none ());
@@ -3229,8 +3243,8 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum
32293243 // Create row container pre-allocated with known column count
32303244 py::list row (numCols);
32313245 for (SQLUSMALLINT col = 1 ; col <= numCols; col++) {
3232- const ColumnInfo& colInfo = columnInfos[col - 1 ];
3233- SQLSMALLINT dataType = colInfo. dataType ;
3246+ // Use prefetched metadata from L1 cache-hot arrays
3247+ SQLSMALLINT dataType = dataTypes[col - 1 ] ;
32343248 SQLLEN dataLen = buffers.indicators [col - 1 ][i];
32353249 if (dataLen == SQL_NULL_DATA) {
32363250 row[col - 1 ] = py::none ();
@@ -3266,11 +3280,10 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum
32663280 case SQL_CHAR:
32673281 case SQL_VARCHAR:
32683282 case SQL_LONGVARCHAR: {
3269- SQLULEN columnSize = colInfo.columnSize ;
3270- HandleZeroColumnSizeAtFetch (columnSize);
3271- uint64_t fetchBufferSize = columnSize + 1 /* null-terminator*/ ;
3283+ SQLULEN columnSize = columnSizes[col - 1 ];
3284+ uint64_t fetchBufferSize = fetchBufferSizes[col - 1 ];
32723285 uint64_t numCharsInData = dataLen / sizeof (SQLCHAR);
3273- bool isLob = colInfo. isLob ;
3286+ bool isLob = isLobs[col - 1 ] ;
32743287 // fetchBufferSize includes null-terminator, numCharsInData doesn't. Hence '<'
32753288 if (!isLob && numCharsInData < fetchBufferSize) {
32763289 row[col - 1 ] = py::str (
@@ -3285,11 +3298,10 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum
32853298 case SQL_WVARCHAR:
32863299 case SQL_WLONGVARCHAR: {
32873300 // TODO: variable length data needs special handling, this logic wont suffice
3288- SQLULEN columnSize = colInfo.columnSize ;
3289- HandleZeroColumnSizeAtFetch (columnSize);
3290- uint64_t fetchBufferSize = columnSize + 1 /* null-terminator*/ ;
3301+ SQLULEN columnSize = columnSizes[col - 1 ];
3302+ uint64_t fetchBufferSize = fetchBufferSizes[col - 1 ];
32913303 uint64_t numCharsInData = dataLen / sizeof (SQLWCHAR);
3292- bool isLob = colInfo. isLob ;
3304+ bool isLob = isLobs[col - 1 ] ;
32933305 // fetchBufferSize includes null-terminator, numCharsInData doesn't. Hence '<'
32943306 if (!isLob && numCharsInData < fetchBufferSize) {
32953307#if defined(__APPLE__) || defined(__linux__)
@@ -3489,9 +3501,8 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum
34893501 case SQL_BINARY:
34903502 case SQL_VARBINARY:
34913503 case SQL_LONGVARBINARY: {
3492- SQLULEN columnSize = colInfo.columnSize ;
3493- HandleZeroColumnSizeAtFetch (columnSize);
3494- bool isLob = colInfo.isLob ;
3504+ SQLULEN columnSize = columnSizes[col - 1 ];
3505+ bool isLob = isLobs[col - 1 ];
34953506 if (!isLob && static_cast <size_t >(dataLen) <= columnSize) {
34963507 row[col - 1 ] = py::bytes (reinterpret_cast <const char *>(
34973508 &buffers.charBuffers [col - 1 ][i * columnSize]),
0 commit comments