3333#define DAE_CHUNK_SIZE 8192
3434#define SQL_MAX_LOB_SIZE 8000
3535
36- // -------------------------------------------------------------------------------------------------
37- // Performance optimization: Cached Python modules and objects
38- // -------------------------------------------------------------------------------------------------
3936namespace PythonObjectCache {
4037 static py::object datetime_class;
4138 static py::object date_class;
@@ -46,27 +43,21 @@ namespace PythonObjectCache {
4643
4744 void initialize () {
4845 if (!cache_initialized) {
49- try {
50- auto datetime_module = py::module_::import (" datetime" );
51- datetime_class = datetime_module.attr (" datetime" );
52- date_class = datetime_module.attr (" date" );
53- time_class = datetime_module.attr (" time" );
54-
55- auto decimal_module = py::module_::import (" decimal" );
56- decimal_class = decimal_module.attr (" Decimal" );
57-
58- auto uuid_module = py::module_::import (" uuid" );
59- uuid_class = uuid_module.attr (" UUID" );
60-
61- cache_initialized = true ;
62- } catch (...) {
63- // If initialization fails, fall back to direct imports
64- cache_initialized = false ;
65- }
46+ auto datetime_module = py::module_::import (" datetime" );
47+ datetime_class = datetime_module.attr (" datetime" );
48+ date_class = datetime_module.attr (" date" );
49+ time_class = datetime_module.attr (" time" );
50+
51+ auto decimal_module = py::module_::import (" decimal" );
52+ decimal_class = decimal_module.attr (" Decimal" );
53+
54+ auto uuid_module = py::module_::import (" uuid" );
55+ uuid_class = uuid_module.attr (" UUID" );
56+
57+ cache_initialized = true ;
6658 }
6759 }
6860
69- // Safe getter functions that fall back to direct import if cache fails
7061 py::object get_datetime_class () {
7162 if (cache_initialized && datetime_class) {
7263 return datetime_class;
@@ -2529,8 +2520,8 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p
25292520
25302521 // Cache decimal separator to avoid repeated system calls
25312522 static const std::string defaultSeparator = " ." ;
2532- static std::string decimalSeparator = GetDecimalSeparator ();
2533- static bool isDefaultDecimalSeparator = (decimalSeparator == defaultSeparator);
2523+ std::string decimalSeparator = GetDecimalSeparator ();
2524+ bool isDefaultDecimalSeparator = (decimalSeparator == defaultSeparator);
25342525
25352526 for (SQLSMALLINT i = 1 ; i <= colCount; ++i) {
25362527 SQLWCHAR columnName[256 ];
@@ -2722,14 +2713,10 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p
27222713 safeLen = bufSize;
27232714 }
27242715 }
2725-
2726- // Use cached decimal separator for locale-specific formatting
27272716 if (isDefaultDecimalSeparator) {
2728- // Fast path: Direct creation without string manipulation
27292717 py::object decimalObj = PythonObjectCache::get_decimal_class ()(py::str (cnum, safeLen));
27302718 row.append (decimalObj);
27312719 } else {
2732- // Slow path: Need separator replacement for locale
27332720 std::string numStr (cnum, safeLen);
27342721 size_t pos = numStr.find (' .' );
27352722 if (pos != std::string::npos) {
@@ -3208,9 +3195,9 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum
32083195 struct ColumnInfo {
32093196 SQLSMALLINT dataType;
32103197 SQLULEN columnSize;
3211- SQLULEN processedColumnSize; // Post-HandleZeroColumnSizeAtFetch processing
3212- uint64_t fetchBufferSize; // Pre-computed buffer size for char/wchar types
3213- bool isLob; // Pre-compute LOB status for O(1) lookup
3198+ SQLULEN processedColumnSize;
3199+ uint64_t fetchBufferSize;
3200+ bool isLob;
32143201 };
32153202 std::vector<ColumnInfo> columnInfos (numCols);
32163203 for (SQLUSMALLINT col = 0 ; col < numCols; col++) {
@@ -3230,15 +3217,7 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum
32303217 std::string decimalSeparator = GetDecimalSeparator (); // Cache decimal separator
32313218 bool isDefaultDecimalSeparator = (decimalSeparator == defaultSeparator);
32323219
3233- // numRowsFetched is the SQL_ATTR_ROWS_FETCHED_PTR attribute. It'll be populated by
3234- // SQLFetchScroll
3235-
3236- // Pre-allocate batch container to avoid 1000-5000 reallocations per batch
3237- // We know the exact batch size (numRowsFetched), so reserve space upfront
32383220 size_t initialSize = rows.size ();
3239-
3240- // Extend the rows list with None placeholders for the entire batch
3241- // This eliminates numRowsFetched reallocations during append operations
32423221 for (SQLULEN i = 0 ; i < numRowsFetched; i++) {
32433222 rows.append (py::none ());
32443223 }
@@ -3250,21 +3229,15 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum
32503229 const ColumnInfo& colInfo = columnInfos[col - 1 ];
32513230 SQLSMALLINT dataType = colInfo.dataType ;
32523231 SQLLEN dataLen = buffers.indicators [col - 1 ][i];
3253- if (dataLen == SQL_NULL_DATA) {
3232+ if (dataLen == SQL_NULL_DATA) {
32543233 row[col - 1 ] = py::none ();
32553234 continue ;
32563235 }
3257- // TODO: variable length data needs special handling, this logic wont suffice
3258- // This value indicates that the driver cannot determine the length of the data
32593236 if (dataLen == SQL_NO_TOTAL) {
32603237 LOG (" Cannot determine the length of the data. Returning NULL value instead."
32613238 " Column ID - {}" , col);
32623239 row[col - 1 ] = py::none ();
32633240 continue ;
3264- } else if (dataLen == SQL_NULL_DATA) {
3265- LOG (" Column data is NULL. Setting None to the result row. Column ID - {}" , col);
3266- row[col - 1 ] = py::none ();
3267- continue ;
32683241 } else if (dataLen == 0 ) {
32693242 // Handle zero-length (non-NULL) data
32703243 if (dataType == SQL_CHAR || dataType == SQL_VARCHAR || dataType == SQL_LONGVARCHAR) {
@@ -3359,10 +3332,9 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum
33593332
33603333 // Use pre-cached decimal separator
33613334 if (isDefaultDecimalSeparator) {
3362- // Fast path: Direct py::str creation without intermediate string
3335+ // Direct py::str creation without intermediate string
33633336 row[col - 1 ] = PythonObjectCache::get_decimal_class ()(py::str (rawData, decimalDataLen));
33643337 } else {
3365- // Slow path: Need separator replacement
33663338 std::string numStr (rawData, decimalDataLen);
33673339 size_t pos = numStr.find (' .' );
33683340 if (pos != std::string::npos) {
@@ -3741,6 +3713,21 @@ SQLRETURN FetchAll_wrap(SqlHandlePtr StatementHandle, py::list& rows) {
37413713 }
37423714 }
37433715
3716+ // If we have LOBs → fall back to row-by-row fetch + SQLGetData_wrap
3717+ if (!lobColumns.empty ()) {
3718+ LOG (" LOB columns detected, using per-row SQLGetData path" );
3719+ while (true ) {
3720+ ret = SQLFetch_ptr (hStmt);
3721+ if (ret == SQL_NO_DATA) break ;
3722+ if (!SQL_SUCCEEDED (ret)) return ret;
3723+
3724+ py::list row;
3725+ SQLGetData_wrap (StatementHandle, numCols, row); // <-- streams LOBs correctly
3726+ rows.append (row);
3727+ }
3728+ return SQL_SUCCESS;
3729+ }
3730+
37443731 ColumnBuffers buffers (numCols, fetchSize);
37453732
37463733 // Bind columns
0 commit comments