2323#include < cstring> // For std::memcpy
2424#include < filesystem>
2525#include < iostream>
26+ #include < stdexcept>
2627#include < utility> // std::forward
2728#include < datetime.h> // CPython datetime API (PyDateTime_IMPORT, PyDateTime_GET_*, etc.)
2829
@@ -3280,12 +3281,18 @@ py::object FetchLobColumnData(SQLHSTMT hStmt, SQLUSMALLINT colIndex, SQLSMALLINT
32803281
32813282 while (true ) {
32823283 ++loopCount;
3283- std::vector<char > chunk (DAE_CHUNK_SIZE , 0 );
3284+ const size_t offset = buffer.size ();
3285+ if (buffer.max_size () - offset < DAE_CHUNK_SIZE ) {
3286+ throw std::length_error (" LOB data exceeds maximum buffer size" );
3287+ }
3288+ // Keep the existing read windows and zero-filled terminator room.
3289+ buffer.resize (offset + DAE_CHUNK_SIZE , 0 );
3290+ char * chunk = buffer.data () + offset;
32843291 SQLLEN actualRead = 0 ;
32853292 {
32863293 // Release the GIL during blocking SQLGetData LOB streaming
32873294 py::gil_scoped_release release;
3288- ret = SQLGetData_ptr (hStmt, colIndex, cType, chunk. data () , DAE_CHUNK_SIZE , &actualRead);
3295+ ret = SQLGetData_ptr (hStmt, colIndex, cType, chunk, DAE_CHUNK_SIZE , &actualRead);
32893296 }
32903297 CaptureFetchDiagnostics (hStmt, ret, messages, true );
32913298
@@ -3328,11 +3335,13 @@ py::object FetchLobColumnData(SQLHSTMT hStmt, SQLUSMALLINT colIndex, SQLSMALLINT
33283335 // Wide characters
33293336 size_t wcharSize = sizeof (SQLWCHAR );
33303337 if (bytesRead >= wcharSize && (bytesRead % wcharSize == 0 )) {
3331- size_t wcharCount = bytesRead / wcharSize;
3332- std::vector<SQLWCHAR > alignedBuf (wcharCount);
3333- std::memcpy (alignedBuf.data (), chunk.data (), bytesRead);
3334- while (wcharCount > 0 && alignedBuf[wcharCount - 1 ] == 0 ) {
3335- --wcharCount;
3338+ while (bytesRead >= wcharSize) {
3339+ SQLWCHAR lastChar;
3340+ // The byte destination need not be aligned for SQLWCHAR.
3341+ std::memcpy (&lastChar, chunk + bytesRead - wcharSize, wcharSize);
3342+ if (lastChar != 0 ) {
3343+ break ;
3344+ }
33363345 bytesRead -= wcharSize;
33373346 }
33383347 if (bytesRead < DAE_CHUNK_SIZE ) {
@@ -3343,8 +3352,8 @@ py::object FetchLobColumnData(SQLHSTMT hStmt, SQLUSMALLINT colIndex, SQLSMALLINT
33433352 }
33443353 }
33453354 }
3355+ buffer.resize (offset + bytesRead);
33463356 if (bytesRead > 0 ) {
3347- buffer.insert (buffer.end (), chunk.begin (), chunk.begin () + bytesRead);
33483357 LOG (" FetchLobColumnData: Appended %zu bytes at loop %d" , bytesRead, loopCount);
33493358 }
33503359 if (ret == SQL_SUCCESS ) {
@@ -6667,6 +6676,20 @@ PYBIND11_MODULE(ddbc_bindings, m) {
66676676 py::arg (" StatementHandle" ), py::arg (" colCount" ), py::arg (" row" ), py::arg (" charEncoding" ),
66686677 py::arg (" wcharEncoding" ), py::arg (" charCtype" ), py::arg (" messages" ) = py::none ());
66696678 m.def (" DDBCSQLMoreResults" , &SQLMoreResults_wrap, " Check for more results in the result set" );
6679+ py::class_<FetchOptions>(m, " _FetchOptions" )
6680+ .def (py::init<const std::string&, const std::string&, int >());
6681+ m.def (" _fetchone_with_options" ,
6682+ [](SqlHandlePtr statement, py::list& row, const FetchOptions& options,
6683+ py::handle messages) {
6684+ return FetchOne_wrap (std::move (statement), row, options.charEncoding ,
6685+ options.wcharEncoding , options.charCtype , messages);
6686+ });
6687+ m.def (" _fetchmany_with_options" ,
6688+ [](SqlHandlePtr statement, py::list& rows, int size, const FetchOptions& options,
6689+ py::handle messages) {
6690+ return FetchMany_wrap (std::move (statement), rows, size, options.charEncoding ,
6691+ options.wcharEncoding , options.charCtype , messages);
6692+ });
66706693 m.def (" DDBCSQLFetchOne" , &FetchOne_wrap, " Fetch one row from the result set" ,
66716694 py::arg (" StatementHandle" ), py::arg (" row" ), py::arg (" charEncoding" ) = " utf-16le" ,
66726695 py::arg (" wcharEncoding" ) = " utf-16le" , py::arg (" charCtype" ) = SQL_C_WCHAR ,
@@ -6778,6 +6801,10 @@ PYBIND11_MODULE(ddbc_bindings, m) {
67786801 // Add a version attribute
67796802 m.attr (" __version__" ) = " 1.0.0" ;
67806803
6804+ m.def (" _apply_output_converters" , &RowFactory::apply_output_converters,
6805+ " Apply a cached converter map after native value materialization" ,
6806+ py::arg (" values" ), py::arg (" converters" ));
6807+
67816808 // Fast Row construction in C++ — replaces Python list comprehension
67826809 m.def (" construct_rows" , &RowFactory::construct_rows,
67836810 " Build Row objects in C++ for fetchall/fetchmany fast path" ,
0 commit comments