Skip to content

FIX: Validate caller-controlled native buffer sizes - #802

Open
gargsaumya wants to merge 47 commits into
mainfrom
saumya/native-size-validation
Open

gargsaumya wants to merge 47 commits into
mainfrom
saumya/native-size-validation

Conversation

@gargsaumya

@gargsaumya gargsaumya commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Work Item / Issue Reference

ADO Work Item: AB#47843


Summary

  • Reject invalid Python row counts before crossing native boundaries.
  • Check parameter allocation arithmetic and metadata cardinality.
  • Tighten binary and wide-character input validation.

No tracking issue exists for this security hardening work.

Copilot AI lite review requested due to automatic review settings September 21, 2026 06:53
@github-actions

github-actions Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

PR Performance Report

⚠️ Performance regression detected

1 database task consistently slowed down across 2 measured environments.

0 IMPROVEMENTS 1 SLOWDOWN 2/2 ENVIRONMENTS

Signal fingerprint

Database task Unix / SQL Server 2022 Unix / SQL Server 2025
Insertion with explicit input sizes 24.8% slower 21.6% slower

The largest recorded phase increases for these tasks are shown below. Phase timings are supporting evidence, not root-cause proof.

Coverage: 2 of 2 environments completed. Advisory result; does not block merging.

Measured timings
Environment Database task Before After Change
Unix / SQL Server 2022 Insertion with explicit input sizes 485.150 ms 604.766 ms +24.8%
Unix / SQL Server 2025 Insertion with explicit input sizes 473.629 ms 582.821 ms +21.6%
Performance diagnostics

Phase times are inclusive diagnostics and must not be added together. They identify where measured time changed, not why it changed.

Unix / SQL Server 2022

Insertion with explicit input sizes: py::execute::cpp_call +23.758 ms; ddbc::SQLExecute_wrap +23.641 ms; ddbc::BindParameters +0.881 ms.

Unix / SQL Server 2025

Insertion with explicit input sizes: py::execute::cpp_call +13.819 ms; ddbc::SQLExecute_wrap +13.705 ms; py::execute::post_execute +0.017 ms.

All database tasks and timings

Unix / SQL Server 2022

Database task Before After Paired change Result
Connection opening 11.240 ms 10.728 ms -0.6% no signal
SELECT queries 1.077 ms 1.113 ms +1.7% no signal
Row insertion 32.691 ms 32.483 ms -0.5% no signal
Executemany inserts 161.633 ms 181.948 ms +13.6% no signal
Fetch-all queries 121.362 ms 122.258 ms +0.9% no signal
Row-by-row fetching 14.491 ms 14.432 ms +1.4% no signal
Batched row fetching 117.548 ms 118.625 ms +0.7% no signal
Transaction commit and rollback 107.107 ms 107.207 ms +0.4% no signal
Arrow row fetching 90.649 ms 96.398 ms +6.8% no signal
100,000-row insertion 445.303 ms 439.361 ms -2.0% no signal
Row fetching in batches of 100 122.741 ms 124.538 ms +1.6% no signal
Row fetching in batches of 10,000 134.755 ms 129.586 ms -0.7% no signal
Repeated positional queries 32.224 ms 32.453 ms +1.9% no signal
Repeated named-parameter queries 34.505 ms 34.508 ms +0.0% no signal
Legacy 100,000-row insertion 353.848 ms 355.241 ms +0.3% no signal
Insertion with explicit input sizes 485.150 ms 604.766 ms +24.8% consistent slowdown
Joined aggregation queries 175.974 ms 176.483 ms +0.1% no signal
Large joined-result fetching 193.007 ms 194.740 ms -0.1% no signal
1.2-million-row fetching 3458.852 ms 3465.294 ms +0.6% no signal
Common table expression queries 5.418 ms 5.410 ms +1.4% no signal
256 KiB VARCHAR(MAX) / fetchall() 1.707 ms 1.465 ms -14.2% no signal
10,000 scalar values / fetchval() (debug disabled) 107.410 ms 107.947 ms +1.6% no signal

Unix / SQL Server 2025

Database task Before After Paired change Result
Connection opening 99.397 ms 98.681 ms -2.0% no signal
SELECT queries 1.090 ms 1.064 ms -3.7% no signal
Row insertion 34.031 ms 34.256 ms +1.2% no signal
Executemany inserts 152.750 ms 171.743 ms +12.8% no signal
Fetch-all queries 120.472 ms 121.242 ms +0.6% no signal
Row-by-row fetching 14.647 ms 14.597 ms -1.4% no signal
Batched row fetching 117.468 ms 118.151 ms +0.7% no signal
Transaction commit and rollback 114.561 ms 114.374 ms +0.0% no signal
Arrow row fetching 93.120 ms 98.232 ms +6.0% no signal
100,000-row insertion 436.726 ms 432.111 ms -0.5% no signal
Row fetching in batches of 100 121.518 ms 123.748 ms +2.5% no signal
Row fetching in batches of 10,000 138.252 ms 127.388 ms -6.4% no signal
Repeated positional queries 33.559 ms 33.420 ms +0.3% no signal
Repeated named-parameter queries 35.887 ms 36.118 ms +0.9% no signal
Legacy 100,000-row insertion 344.183 ms 347.024 ms +0.8% no signal
Insertion with explicit input sizes 473.629 ms 582.821 ms +21.6% consistent slowdown
Joined aggregation queries 161.496 ms 160.842 ms -0.1% no signal
Large joined-result fetching 181.620 ms 181.513 ms +0.2% no signal
1.2-million-row fetching 3392.021 ms 3423.966 ms +0.9% no signal
Common table expression queries 5.115 ms 5.210 ms +2.1% no signal
256 KiB VARCHAR(MAX) / fetchall() 1.446 ms 1.382 ms -4.3% no signal
10,000 scalar values / fetchval() (debug disabled) 106.023 ms 107.799 ms +1.4% no signal
Build and measurement details

ADO build 184377

PR head: 44017f9e31d9288ee1dde2c318d8d2a0ea0fc811
Base: 9424705251baf8ded41a8a4968216b1fd9b00929
Measured merge: 6f205f329e185764f89466feeeeb3df61a0689d5

  • Unix / SQL Server 2022: Python 3.12.3, x86_64, SQL 16.0.4305.5; 5 paired comparisons and 1 warmup.
  • Unix / SQL Server 2025: Python 3.12.3, x86_64, SQL 17.0.5015.4; 5 paired comparisons and 1 warmup.

A consistent change requires more than 20% median paired movement, at least 1 ms between the median runtimes, and at least 80% of pairs exceeding the relative threshold in the same direction. A slowdown without enough pair agreement is reported as inconsistent.

The displayed change is the median of paired before-and-after ratios. It is not recalculated from the two displayed median runtimes.

Both revisions use profiling-enabled builds on the same agent and database, with alternating order and discarded warmups. Results are diagnostic and do not represent production-wheel latency.

Raw samples and logs are attached to the ADO run as profiler-* artifacts.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Unresolved row-count and binary validation gaps remain, and an existing Arrow batch-size test requires reconciliation.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity · 1 Medium severity

Open (2)
What changed in this PR

Hardens Python-to-native boundary validation for row counts, allocations, metadata, and binary inputs.

Changes:

  • Adds row-count, boolean, and input-size validation.
  • Adds native allocation overflow and metadata checks.
  • Tightens binary and wide-character handling with regression tests.
File Description
tests/​test_024_bulkcopy_arrow.py Tests boolean batch-size rejection.
tests/​test_004_cursor.py Tests cursor size and metadata validation.
mssql_python/​pybind/​ddbc_bindings.cpp Adds native allocation and parameter validation.
mssql_python/​cursor.py Validates row counts and input sizes.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread mssql_python/cursor.py
Comment thread mssql_python/pybind/ddbc_bindings.cpp
Copilot AI review requested due to automatic review settings September 21, 2026 07:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Native paths still permit dangerous negative or extremely large allocations, and arrow_reader() does not reject invalid sizes synchronously.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 4 High severity · 1 Medium severity

Open (5)

Comment thread mssql_python/cursor.py
Comment thread mssql_python/pybind/ddbc_bindings.cpp Outdated
Comment thread mssql_python/pybind/ddbc_bindings.cpp
@github-actions

github-actions Bot commented Sep 21, 2026 •

Copy link
Copy Markdown

📊 Code Coverage Report

🔥 Diff Coverage

86%


🎯 Overall Coverage

85%


📈 Total Lines Covered: 10238 out of 11917
📁 Project: mssql-python


Diff Coverage

Diff: main...HEAD, staged and unstaged changes

  • mssql_python/cursor.py (79.2%): Missing lines 62-74,2677,2697,2725
  • mssql_python/pybind/connection/connection.cpp (100%)
  • mssql_python/pybind/ddbc_bindings.cpp (86.7%): Missing lines 329-330,339-340,346-347,354-355,363-364,425-427,437-438,443-444,449-450,452,464-465,472-473,564,607-608,615-623,637-639,649-653,667-668,675-676,872,1972,2578-2581,2652,2654-2655,2706,2777-2781,2793,2810-2811,2844-2845,2849,2853-2854,2861,3370-3371,3408-3410,3589,3593,3599,3634,3707-3708,3725-3727,4720,4777,4795-4797,4819,4851-4852,4854,4857,4897-4898,5317,5746,5784-5791,5813-5814,5843-5845,5850-5851,5865-5866,5874-5875,5889,5974-5976,6133,6198-6204,6293
  • mssql_python/pybind/ddbc_bindings.h (92.9%): Missing lines 350-351
  • mssql_python/pybind/param_detect.hpp (85.8%): Missing lines 145,148,151,154,241-242,246-248,279-280,284-286,320-321,329-330,335-336,344-345,354-355,481-482

Summary

  • Total: 1294 lines
  • Missing: 176 lines
  • Coverage: 86%

mssql_python/cursor.py

Lines 58-78

  58 MAX_NATIVE_PARAMETER_SIZE: int = 256 * 1024 * 1024
  59 
  60 
  61 def _encoded_length_exceeds(value: str, encoding: str, limit: int) -> bool:
! 62     encoder = codecs.getincrementalencoder(encoding)(errors="strict")
! 63     total = 0
! 64     chunk_size = 4096
! 65     value_length = str.__len__(value)
! 66     for offset in range(0, value_length, chunk_size):
! 67         end = min(offset + chunk_size, value_length)
! 68         chunk = str.__getitem__(value, slice(offset, end))
! 69         total += len(encoder.encode(chunk, final=end == value_length))
! 70         if total > limit:
! 71             return True
! 72     if value_length == 0:
! 73         total += len(encoder.encode("", final=True))
! 74     return total > limit
  75 
  76 
  77 def _utf16_length_exceeds(value: str, limit: int) -> bool:
  78     bounded_value = str.__getitem__(value, slice(0, limit + 1))

Lines 2673-2681

  2673                         binary_values = [
  2674                             value for value in column if isinstance(value, (bytes, bytearray))
  2675                         ]
  2676                         if c_type == ddbc_sql_const.SQL_CHAR.value:
! 2677                             text_is_large = any(
  2678                                 _encoded_length_exceeds(
  2679                                     value,
  2680                                     encoding_settings["encoding"],
  2681                                     MAX_INLINE_CHAR,

Lines 2693-2701

  2693                         requires_row_fallback = bool(
  2694                             narrow_text_type and text_values and binary_values
  2695                         )
  2696                         if narrow_text_type and binary_values and not text_values:
! 2697                             c_type = ddbc_sql_const.SQL_CHAR.value
  2698                         is_dae = text_is_large or binary_is_large
  2699 
  2700                     # Sanitize precision/scale for numeric types
  2701                     if sql_type in (

Lines 2721-2729

  2721 
  2722                         # For SQL Server VARBINARY(MAX), we need to use large object binding
  2723                         if max_binary_size > MAX_INLINE_BINARY:
  2724                             if sql_type != ddbc_sql_const.SQL_SS_UDT.value:
! 2725                                 sql_type = ddbc_sql_const.SQL_LONGVARBINARY.value
  2726                             is_dae = True
  2727 
  2728                         # Update column_size to actual maximum size if it's larger
  2729                         # Always ensure at least a minimum size of 1 for empty strings

mssql_python/pybind/ddbc_bindings.cpp

Lines 325-334

  325 
  326 template <typename ParamType>
  327 ParamType* AllocateParamBufferArray(std::vector<std::shared_ptr<void>>& paramBuffers,
  328                                     size_t count) {
! 329     if (count > std::numeric_limits<size_t>::max() / sizeof(ParamType)) {
! 330         ThrowStdException("Parameter buffer size is too large");
  331     }
  332     std::shared_ptr<ParamType> buffer(new ParamType[count], std::default_delete<ParamType[]>());
  333     ParamType* raw = buffer.get();
  334     paramBuffers.push_back(buffer);

Lines 335-344

  335     return raw;
  336 }
  337 
  338 size_t CheckedAddSize(size_t left, size_t right, const char* errorMessage) {
! 339     if (left > std::numeric_limits<size_t>::max() - right) {
! 340         ThrowStdException(errorMessage);
  341     }
  342     return left + right;
  343 }

Lines 342-351

  342     return left + right;
  343 }
  344 
  345 size_t CheckedMultiplySize(size_t left, size_t right, const char* errorMessage) {
! 346     if (left != 0 && right > std::numeric_limits<size_t>::max() / left) {
! 347         ThrowStdException(errorMessage);
  348     }
  349     return left * right;
  350 }

Lines 350-359

  350 }
  351 
  352 size_t CheckedFetchColumnSize(SQLULEN columnSize) {
  353     const size_t result = static_cast<size_t>(columnSize);
! 354     if (static_cast<SQLULEN>(result) != columnSize) {
! 355         ThrowStdException("Column size is too large");
  356     }
  357     return result;
  358 }

Lines 359-368

  359 
  360 SQLLEN CheckedFetchBufferLength(size_t elementCount, size_t elementSize) {
  361     const size_t byteCount =
  362         CheckedMultiplySize(elementCount, elementSize, "Column fetch stride is too large");
! 363     if (byteCount > static_cast<size_t>(std::numeric_limits<SQLLEN>::max())) {
! 364         ThrowStdException("Column fetch stride is too large");
  365     }
  366     return static_cast<SQLLEN>(byteCount);
  367 }

Lines 421-431

  421         case SQL_C_CHAR:
  422         case SQL_C_BINARY:
  423             return CheckedAddSize(info.columnSize, 1, "Character parameter size is too large");
  424         case SQL_C_BIT:
! 425             return sizeof(char);
! 426         case SQL_C_STINYINT:
! 427         case SQL_C_USHORT:
  428             return sizeof(unsigned short);
  429         case SQL_C_SBIGINT:
  430         case SQL_C_SLONG:
  431         case SQL_C_UBIGINT:

Lines 433-456

  433             return sizeof(int64_t);
  434         case SQL_C_FLOAT:
  435             return sizeof(float);
  436         case SQL_C_TYPE_DATE:
! 437             return sizeof(SQL_DATE_STRUCT);
! 438         case SQL_C_TYPE_TIME:
  439             return sizeof(SQL_TIME_STRUCT);
  440         case SQL_C_TYPE_TIMESTAMP:
  441             return sizeof(SQL_TIMESTAMP_STRUCT);
  442         case SQL_C_SS_TIMESTAMPOFFSET:
! 443             return sizeof(DateTimeOffset);
! 444         case SQL_C_NUMERIC:
  445             return sizeof(SQL_NUMERIC_STRUCT);
  446         case SQL_C_GUID:
  447             return sizeof(SQLGUID);
  448         case SQL_C_DEFAULT:
! 449             return sizeof(char);
! 450         default:
  451             ThrowStdException("Unsupported C type for parameter array allocation");
! 452     }
  453     return 0;
  454 }
  455 
  456 template <typename ElementType>

Lines 460-469

  460         CheckedMultiplySize(rowIndex, rowStride, "Arrow source offset is too large");
  461     const size_t rowCapacity = CheckedMultiplySize(
  462         rowStride, sizeof(ElementType), "Arrow source capacity is too large");
  463     if (offset > buffer.size() || rowStride > buffer.size() - offset ||
! 464         dataBytes > rowCapacity) {
! 465         ThrowStdException("Driver data length exceeds the allocated fetch buffer");
  466     }
  467     return offset;
  468 }

Lines 468-477

  468 }
  469 
  470 template <typename ElementType>
  471 std::unique_ptr<ElementType[]> AllocateUniqueArray(size_t count, const char* errorMessage) {
! 472     if (count > std::numeric_limits<size_t>::max() / sizeof(ElementType)) {
! 473         ThrowStdException(errorMessage);
  474     }
  475     return std::make_unique<ElementType[]>(count);
  476 }

Lines 560-568

  560                                                   const char* errorMessage) {
  561     ReserveNativeFetchBytes(reservedBytes, count, sizeof(ElementType));
  562     return AllocateUniqueArray<ElementType>(count, errorMessage);
  563 }
! 564 
  565 std::string DescribeChar(unsigned char ch) {
  566     if (ch >= 32 && ch <= 126) {
  567         return std::string("'") + static_cast<char>(ch) + "'";
  568     } else {

Lines 603-612

  603         if (!chunk) throw py::error_already_set();
  604         py::object encoded = encoder.attr("encode")(chunk, end == length);
  605         char* data = nullptr;
  606         Py_ssize_t size = 0;
! 607         if (PyBytes_AsStringAndSize(encoded.ptr(), &data, &size) != 0) {
! 608             throw py::error_already_set();
  609         }
  610         if (size > 0) {
  611             SQLRETURN rc = put_data_fn(data, static_cast<SQLLEN>(size));
  612             if (!SQL_SUCCEEDED(rc)) return rc;

Lines 611-627

  611             SQLRETURN rc = put_data_fn(data, static_cast<SQLLEN>(size));
  612             if (!SQL_SUCCEEDED(rc)) return rc;
  613         }
  614     }
! 615     if (length == 0) {
! 616         py::object encoded = encoder.attr("encode")(py::str(), true);
! 617         char* data = nullptr;
! 618         Py_ssize_t size = 0;
! 619         if (PyBytes_AsStringAndSize(encoded.ptr(), &data, &size) != 0) {
! 620             throw py::error_already_set();
! 621         }
! 622         if (size > 0) return put_data_fn(data, static_cast<SQLLEN>(size));
! 623         return put_data_fn(nullptr, 0);
  624     }
  625     return SQL_SUCCESS;
  626 }

Lines 633-643

  633 
  634 static SQLRETURN StreamDAEParameter(SQLHSTMT hStmt, const ParamInfo& info,
  635                                     const std::string& charEncoding) {
  636     PyObject* value = info.dataPtr.ptr();
! 637     if (!value || value == Py_None) {
! 638         py::gil_scoped_release release;
! 639         return SQLPutData_ptr(hStmt, nullptr, 0);
  640     }
  641 
  642     auto putImmutableData = [&](SQLPOINTER data, SQLLEN length) {
  643         py::gil_scoped_release release;

Lines 645-657

  645     };
  646     if (PyUnicode_Check(value)) {
  647         if (info.paramCType == SQL_C_WCHAR) {
  648             return stream_unicode_dae_chunks(value, "utf-16-le", putImmutableData);
! 649         }
! 650         if (info.paramCType == SQL_C_CHAR) {
! 651             return stream_unicode_dae_chunks(value, charEncoding, putImmutableData);
! 652         }
! 653         ThrowStdException("DAE only supports text C types for str values");
  654     }
  655     if (PyBytes_Check(value)) {
  656         return stream_dae_chunks(PyBytes_AS_STRING(value),
  657                                  static_cast<size_t>(PyBytes_GET_SIZE(value)),

Lines 663-672

  663         std::vector<char> chunk(std::min(static_cast<size_t>(DAE_CHUNK_SIZE), totalBytes));
  664         for (size_t offset = 0; offset < totalBytes; offset += chunk.size()) {
  665             const size_t currentSize = static_cast<size_t>(PyByteArray_GET_SIZE(value));
  666             const size_t length = std::min(chunk.size(), totalBytes - offset);
! 667             if (currentSize < offset + length) {
! 668                 ThrowStdException("bytearray changed size during DAE streaming");
  669             }
  670             std::copy_n(PyByteArray_AS_STRING(value) + offset, length, chunk.data());
  671             SQLRETURN rc = putImmutableData(chunk.data(), static_cast<SQLLEN>(length));
  672             if (!SQL_SUCCEEDED(rc)) return rc;

Lines 671-680

  671             SQLRETURN rc = putImmutableData(chunk.data(), static_cast<SQLLEN>(length));
  672             if (!SQL_SUCCEEDED(rc)) return rc;
  673         }
  674         return SQL_SUCCESS;
! 675     }
! 676     ThrowStdException("DAE only supports str, bytes, or bytearray values");
  677     return SQL_ERROR;
  678 }
  679 
  680 // GH-610: Resolve SQL type for a NULL parameter using per-handle cache.

Lines 868-876

  868                     if (py::isinstance<py::str>(param)) {
  869                         // Encode Unicode string using the specified encoding
  870                         try {
  871                             py::object encoded =
! 872                                 EncodeUnicodeValue(param.ptr(), charEncoding);
  873                             encodedStr = encoded.cast<std::string>();
  874                             LOG("BindParameters: param[%d] SQL_C_CHAR - Encoded with '%s', "
  875                                 "size=%zu bytes",
  876                                 paramIndex, charEncoding.c_str(), encodedStr.size());

Lines 1968-1976

  1968 }
  1969 
  1970 SQLRETURN SqlHandle::freeHandle() {
  1971     PERF_TIMER("SqlHandle::free");
! 1972     clearResult();
  1973     bool pythonShuttingDown = is_python_finalizing();
  1974     bool skipDuringShutdown = _type == SQL_HANDLE_STMT || _type == SQL_HANDLE_DBC;
  1975 #ifdef _WIN32
  1976     // The static ENV is destroyed during DLL_PROCESS_DETACH, after Python

Lines 2574-2585

  2574         SQLRETURN describeRc = PreResolveUdtTypes(hStmt, paramInfos);
  2575         if (!SQL_SUCCEEDED(describeRc)) return describeRc;
  2576         // GH-627: resolve unknown NULL array param SQL types before binding any param.
  2577         PreResolveUnknownNullTypes(handle, hStmt, paramInfos);
! 2578         size_t reservedParameterBytes = 0;
! 2579         for (const ParamInfo& info : paramInfos) {
! 2580             ReserveNativeParameterBytes(reservedParameterBytes, paramSetSize,
! 2581                                         ParameterArrayElementSize(info));
  2582             ReserveNativeParameterBytes(reservedParameterBytes, paramSetSize, sizeof(SQLLEN));
  2583         }
  2584         for (int paramIndex = 0; paramIndex < columnwise_params.size(); ++paramIndex) {
  2585             const py::list& columnValues = columnwise_params[paramIndex].cast<py::list>();

Lines 2648-2659

  2648                         elementWidth, sizeof(SQLWCHAR),
  2649                         "Wide-character parameter length is too large");
  2650                     if (bufferBytes > static_cast<size_t>(std::numeric_limits<SQLLEN>::max())) {
  2651                         ThrowStdException("Wide-character parameter length is too large");
! 2652                     }
  2653                     SQLWCHAR* wcharArray = AllocateParamBufferArray<SQLWCHAR>(
! 2654                         tempBuffers,
! 2655                         CheckedMultiplySize(paramSetSize, elementWidth,
  2656                                             "Wide-character parameter buffer is too large"));
  2657                     strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
  2658                     for (size_t i = 0; i < paramSetSize; ++i) {
  2659                         if (columnValues[i].is_none()) {

Lines 2702-2710

  2702                     LOG("BindParameterArray: SQL_C_WCHAR bound - "
  2703                         "param_index=%d",
  2704                         paramIndex);
  2705                     dataPtr = wcharArray;
! 2706                     bufferLength = static_cast<SQLLEN>(bufferBytes);
  2707                     break;
  2708                 }
  2709                 case SQL_C_TINYINT:
  2710                 case SQL_C_UTINYINT: {

Lines 2773-2785

  2773                 case SQL_C_BINARY: {
  2774                     LOG("BindParameterArray: Binding SQL_C_CHAR/BINARY array - "
  2775                         "param_index=%d, count=%zu, column_size=%zu, encoding='%s'",
  2776                         paramIndex, paramSetSize, info.columnSize, charEncoding.c_str());
! 2777                     const size_t elementWidth = CheckedAddSize(
! 2778                         info.columnSize, 1, "Character parameter size is too large");
! 2779                     if (elementWidth > static_cast<size_t>(std::numeric_limits<SQLLEN>::max())) {
! 2780                         ThrowStdException("Character parameter length is too large");
! 2781                     }
  2782                     char* charArray = AllocateParamBufferArray<char>(
  2783                         tempBuffers,
  2784                         CheckedMultiplySize(paramSetSize, elementWidth,
  2785                                             "Character parameter buffer is too large"));

Lines 2789-2797

  2789                             strLenOrIndArray[i] = SQL_NULL_DATA;
  2790                             std::memset(charArray + i * (info.columnSize + 1), 0,
  2791                                         info.columnSize + 1);
  2792                         } else {
! 2793                             if (info.paramCType == SQL_C_BINARY &&
  2794                                 !py::isinstance<py::bytes>(columnValues[i]) &&
  2795                                 !py::isinstance<py::bytearray>(columnValues[i])) {
  2796                                 ThrowStdException(MakeParamMismatchErrorStr(info.paramCType,
  2797                                                                             paramIndex));

Lines 2806-2815

  2806                                     encoded =
  2807                                         EncodeUnicodeValue(columnValues[i].ptr(), charEncoding);
  2808                                     if (PyBytes_AsStringAndSize(encoded.ptr(), &encodedData,
  2809                                                                 &encodedSize) != 0) {
! 2810                                         throw py::error_already_set();
! 2811                                     }
  2812                                     LOG("BindParameterArray: param[%d] row[%zu] SQL_C_CHAR - "
  2813                                         "Encoded with '%s', "
  2814                                         "size=%zu bytes",
  2815                                         paramIndex, i, charEncoding.c_str(),

Lines 2840-2858

  2840                                 ThrowStdException(
  2841                                     MakeParamMismatchErrorStr(info.paramCType, paramIndex));
  2842                             }
  2843 
! 2844                             const size_t dataSize = static_cast<size_t>(encodedSize);
! 2845                             if (dataSize > info.columnSize) {
  2846                                 LOG("BindParameterArray: String/binary too "
  2847                                     "long - param_index=%d, row=%zu, size=%zu, "
  2848                                     "max=%zu",
! 2849                                     paramIndex, i, dataSize, info.columnSize);
  2850                                 ThrowStdException("Input exceeds column size at index " +
  2851                                                   std::to_string(i));
  2852                             }
! 2853                             std::copy_n(encodedData, dataSize, charArray + i * elementWidth);
! 2854                             strLenOrIndArray[i] = static_cast<SQLLEN>(dataSize);
  2855                         }
  2856                     }
  2857                     LOG("BindParameterArray: SQL_C_CHAR/BINARY bound - "
  2858                         "param_index=%d",

Lines 2857-2865

  2857                     LOG("BindParameterArray: SQL_C_CHAR/BINARY bound - "
  2858                         "param_index=%d",
  2859                         paramIndex);
  2860                     dataPtr = charArray;
! 2861                     bufferLength = static_cast<SQLLEN>(elementWidth);
  2862                     break;
  2863                 }
  2864                 case SQL_C_BIT: {
  2865                     LOG("BindParameterArray: Binding SQL_C_BIT array - "

Lines 3366-3375

  3366             std::vector<ParamInfo> rowParamInfos = paramInfos;
  3367             for (size_t paramIndex = 0; paramIndex < rowParamInfos.size(); ++paramIndex) {
  3368                 if (rowParams[paramIndex].is_none()) {
  3369                     rowParamInfos[paramIndex].paramCType = SQL_C_DEFAULT;
! 3370                     rowParamInfos[paramIndex].isDAE = false;
! 3371                     rowParamInfos[paramIndex].dataPtr = py::none();
  3372                 }
  3373             }
  3374             std::vector<std::shared_ptr<void>> paramBuffers;
  3375             rc = BindParameters(*statementHandle, hStmt, rowParams, rowParamInfos,

Lines 3404-3414

  3404                     return rc;
  3405                 }
  3406                 if (rc != SQL_NEED_DATA) break;
  3407 
! 3408                 const ParamInfo* matchedInfo = reinterpret_cast<const ParamInfo*>(token);
! 3409                 const ParamInfo* first = rowParamInfos.data();
! 3410                 const ParamInfo* last = first + rowParamInfos.size();
  3411                 if (matchedInfo < first || matchedInfo >= last) {
  3412                     ThrowStdException(
  3413                         "SQLExecuteMany: unrecognized paramToken from SQLParamData");
  3414                 }

Lines 3585-3597

  3585                                  table.empty() ? 0 : SQL_NTS, scope, nullable);
  3586 }
  3587 
  3588 std::shared_ptr<PendingArrowRows> PendingRowsForNextFetch(SqlHandlePtr handle) {
! 3589     auto pending = handle->pendingArrowRows();
  3590     if (pending) {
  3591         pending->hasSelectedRow = false;
  3592         if (pending->next == pending->count) {
! 3593             handle->setPendingArrowRows(nullptr);
  3594             return nullptr;
  3595         }
  3596     }
  3597     return pending;

Lines 3595-3603

  3595         }
  3596     }
  3597     return pending;
  3598 }
! 3599 
  3600 SQLRETURN ReadSelectedArrowRow(SqlHandlePtr handle, PendingArrowRows& pending,
  3601                                 SQLUSMALLINT columnCount, py::list& row,
  3602                                 const std::string& charEncoding, py::handle messages);

Lines 3630-3638

  3630                               bool isWideChar, bool isBinary, const std::string& charEncoding,
  3631                               py::handle messages) {
  3632     PERF_TIMER("FetchLobColumnData");
  3633     std::vector<char> buffer;
! 3634     size_t dataSize = 0;
  3635     size_t reservedBytes = 0;
  3636     ReserveNativeFetchBytes(reservedBytes, DAE_CHUNK_SIZE, sizeof(char));
  3637     SQLRETURN ret = SQL_SUCCESS_WITH_INFO;
  3638     int loopCount = 0;

Lines 3703-3712

  3703             }
  3704         }
  3705         if (bytesRead > 0) {
  3706             const size_t previousSize = dataSize;
! 3707             dataSize = ExtendNativeFetchBuffer(buffer, dataSize, bytesRead, reservedBytes);
! 3708             std::copy_n(chunk.data(), bytesRead, buffer.data() + previousSize);
  3709             LOG("FetchLobColumnData: Appended %zu bytes at loop %d", bytesRead, loopCount);
  3710         }
  3711         if (ret == SQL_SUCCESS) {
  3712             LOG("FetchLobColumnData: SQL_SUCCESS - no more data at loop %d", loopCount);

Lines 3721-3731

  3721         }
  3722         return py::str("");
  3723     }
  3724     if (isWideChar) {
! 3725         if (dataSize % sizeof(SQLWCHAR) != 0) {
! 3726             ThrowStdException("Wide-character LOB data has an invalid byte length");
! 3727         }
  3728         int byteOrder = -1;
  3729         PyObject* decoded =
  3730             PyUnicode_DecodeUTF16(buffer.data(), static_cast<Py_ssize_t>(dataSize),
  3731                                   "strict", &byteOrder);

Lines 4716-4724

  4716                     const size_t fetchBufferSize = CheckedAddSize(
  4717                         baseColumnSize, 1, "Column fetch stride is too large");
  4718                     ResizeNativeFetchBuffer(buffers.wcharBuffers[col - 1],
  4719                                             CheckedMultiplySize(fetchSize, fetchBufferSize,
! 4720                                                                 "Native fetch buffer is too large"),
  4721                                             reservedBytes);
  4722                     ret = SQLBindCol_ptr(
  4723                         hStmt, col, SQL_C_WCHAR, buffers.wcharBuffers[col - 1].data(),
  4724                         CheckedFetchBufferLength(fetchBufferSize, sizeof(SQLWCHAR)),

Lines 4773-4781

  4773                                      buffers.smallIntBuffers[col - 1].data(), sizeof(SQLSMALLINT),
  4774                                      buffers.indicators[col - 1].data());
  4775                 break;
  4776             case SQL_TINYINT:
! 4777                 ResizeNativeFetchBuffer(buffers.charBuffers[col - 1], fetchSize, reservedBytes);
  4778                 ret = SQLBindCol_ptr(hStmt, col, SQL_C_TINYINT, buffers.charBuffers[col - 1].data(),
  4779                                      sizeof(SQLCHAR), buffers.indicators[col - 1].data());
  4780                 break;
  4781             case SQL_BIT:

Lines 4791-4801

  4791             case SQL_DECIMAL:
  4792             case SQL_NUMERIC:
  4793                 ResizeNativeFetchBuffer(
  4794                     buffers.charBuffers[col - 1],
! 4795                     CheckedMultiplySize(fetchSize, MAX_DIGITS_IN_NUMERIC,
! 4796                                         "Native fetch buffer is too large"),
! 4797                     reservedBytes);
  4798                 ret = SQLBindCol_ptr(hStmt, col, SQL_C_CHAR, buffers.charBuffers[col - 1].data(),
  4799                                      MAX_DIGITS_IN_NUMERIC * sizeof(SQLCHAR),
  4800                                      buffers.indicators[col - 1].data());
  4801                 break;

Lines 4815-4823

  4815                     hStmt, col, SQL_C_TYPE_TIMESTAMP, buffers.timestampBuffers[col - 1].data(),
  4816                     sizeof(SQL_TIMESTAMP_STRUCT), buffers.indicators[col - 1].data());
  4817                 break;
  4818             case SQL_BIGINT:
! 4819                 ResizeNativeFetchBuffer(buffers.bigIntBuffers[col - 1], fetchSize, reservedBytes);
  4820                 ret =
  4821                     SQLBindCol_ptr(hStmt, col, SQL_C_SBIGINT, buffers.bigIntBuffers[col - 1].data(),
  4822                                    sizeof(SQLBIGINT), buffers.indicators[col - 1].data());
  4823                 break;

Lines 4847-4861

  4847                 HandleZeroColumnSizeAtFetch(columnSize);
  4848                 const size_t fetchBufferSize = CheckedFetchColumnSize(columnSize);
  4849                 ResizeNativeFetchBuffer(buffers.charBuffers[col - 1],
  4850                                         CheckedMultiplySize(fetchSize, fetchBufferSize,
! 4851                                                             "Native fetch buffer is too large"),
! 4852                                         reservedBytes);
  4853                 ret = SQLBindCol_ptr(hStmt, col, SQL_C_BINARY, buffers.charBuffers[col - 1].data(),
! 4854                                      CheckedFetchBufferLength(fetchBufferSize, 1),
  4855                                      buffers.indicators[col - 1].data());
  4856                 break;
! 4857             }
  4858             case SQL_SS_TIMESTAMPOFFSET:
  4859                 ResizeNativeFetchBuffer(buffers.datetimeoffsetBuffers[col - 1], fetchSize,
  4860                                         reservedBytes);
  4861                 ret = SQLBindCol_ptr(hStmt, col, SQL_C_SS_TIMESTAMPOFFSET,

Lines 4893-4902

  4893 SQLRETURN ConvertBufferedRows(SQLHSTMT hStmt, ColumnBuffers& buffers, const Metadata& columnNames,
  4894                          py::list& rows, SQLUSMALLINT numCols, SQLULEN& numRowsFetched,
  4895                          const std::vector<SQLUSMALLINT>& lobColumns,
  4896                          const std::string& charEncoding = "utf-16le", int charCtype = SQL_C_WCHAR,
! 4897                          py::handle messages = {}, SQLULEN firstRow = 0,
! 4898                          int bufferRows = 0) {
  4899     // Pre-cache column metadata to avoid repeated dictionary lookups.
  4900     // The vectors below are consumed later by construct_rows, so they are
  4901     // declared at function scope; only the population work is wrapped in the
  4902     // cache_column_metadata timer's block (an earlier version put the timer at

Lines 5313-5321

  5313                     CheckedAddSize(columnSize, 1, "Native fetch row size is too large"),
  5314                     sizeof(SQLWCHAR), "Native fetch row size is too large");
  5315                 break;
  5316             case SQL_INTEGER:
! 5317                 columnBytes = sizeof(SQLINTEGER);
  5318                 break;
  5319             case SQL_SMALLINT:
  5320                 columnBytes = sizeof(SQLSMALLINT);
  5321                 break;

Lines 5742-5750

  5742         case SQL_DECIMAL:
  5743         case SQL_NUMERIC:
  5744             return ReadArrowScalar(stmt, ordinal, SQL_C_CHAR, buffers.charBuffers[column],
  5745                                      indicator, reservedBytes, messages, MAX_DIGITS_IN_NUMERIC);
! 5746         case SQL_DOUBLE:
  5747         case SQL_FLOAT:
  5748             return ReadArrowScalar(stmt, ordinal, SQL_C_DOUBLE, buffers.doubleBuffers[column],
  5749                                      indicator, reservedBytes, messages);
  5750         case SQL_TIMESTAMP:

Lines 5780-5795

  5780                                     py::handle messages) {
  5781     if (pending.hasLobs) {
  5782         size_t reservedBytes = pending.reservedBytes;
  5783         for (; pending.lobColumn < columnCount; ++pending.lobColumn) {
! 5784             auto column = columns[pending.lobColumn].cast<py::dict>();
! 5785             const SQLRETURN ret = ReadArrowLobColumn(handle->get(), pending,
! 5786                                       column["DataType"].cast<SQLSMALLINT>(),
! 5787                                       pending.lobColumn, reservedBytes, messages);
! 5788             CheckFetchError(handle, ret);
! 5789             if (!SQL_SUCCEEDED(ret)) {
! 5790                 return ret;
! 5791             }
  5792         }
  5793     }
  5794     return SQL_SUCCESS;
  5795 }

Lines 5809-5818

  5809     if (!SQL_SUCCEEDED(ret)) {
  5810         return ret;
  5811     }
  5812     py::list rows;
! 5813     SQLULEN end = pending.selectedRow + 1;
! 5814     ret = ConvertBufferedRows(handle->get(), pending.buffers, columns, rows, columnCount, end,
  5815                                {}, charEncoding, pending.charCtype, messages,
  5816                                pending.selectedRow, pending.fetchSize);
  5817     if (SQL_SUCCEEDED(ret)) {
  5818         for (auto value : rows[0].cast<py::list>()) {

Lines 5839-5855

  5839         return ret;
  5840     }
  5841     SQLULEN end = pending->next + std::min(limit, pending->count - pending->next);
  5842     ret = ConvertBufferedRows(handle->get(), pending->buffers, columns, rows, numCols, end,
! 5843                                {}, charEncoding, pending->charCtype, messages,
! 5844                                pending->next, pending->fetchSize);
! 5845     if (SQL_SUCCEEDED(ret)) {
  5846         pending->next = end;
  5847         if (pending->next == pending->count) {
  5848             handle->setPendingArrowRows(nullptr);
  5849         }
! 5850     }
! 5851     return ret;
  5852 }
  5853 
  5854 int32_t days_from_civil(int y, int m, int d) {
  5855     // Implements the "days_from_civil" algorithm by Howard Hinnant

Lines 5861-5870

  5861     const unsigned doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;           // [0, 146096]
  5862     return era * 146097 + static_cast<int>(doe) - 719468;
  5863 }
  5864 
! 5865 SQLRETURN FetchArrowBatch_wrap(SqlHandlePtr StatementHandle, py::list& capsules,
! 5866                                py::handle arrowBatchSizeArg, int charCtype,
  5867                                py::handle messages = {}) {
  5868     PERF_TIMER("FetchArrowBatch_wrap");
  5869     const int arrowBatchSize =
  5870         ValidateNativeRowCountArgument(arrowBatchSizeArg, "Arrow batch size", true);

Lines 5870-5879

  5870         ValidateNativeRowCountArgument(arrowBatchSizeArg, "Arrow batch size", true);
  5871     const size_t batchSize = static_cast<size_t>(arrowBatchSize);
  5872     const size_t offsetCount = CheckedAddSize(batchSize, 1, "Arrow batch size is too large");
  5873     const size_t initialVarDataSize =
! 5874         CheckedMultiplySize(batchSize, 42, "Arrow batch size is too large");
! 5875     const size_t bitmapSize =
  5876         CheckedAddSize(batchSize, 7, "Arrow batch size is too large") / 8;
  5877     // Fetch narrow char data as SQL_C_CHAR if on Linux/macOS and configured by the user
  5878     charCtype = EffectiveCharCtypeForFetch(charCtype, "utf-8");
  5879     if (arrowBatchSize > 0) {

Lines 5885-5893

  5885     }
  5886 
  5887     // An overly large fetch size doesn't seem to help performance.
  5888     int fetchSize = pending ? pending->fetchSize
! 5889                            : arrowBatchSize > 0 ? std::min(arrowBatchSize, 64) : 1;
  5890 
  5891     SQLRETURN ret = SQL_ERROR;
  5892     ResultMetadataFailureGuard metadataFailure(StatementHandle->resultMetadata, ret);
  5893     SQLHSTMT hStmt = StatementHandle->get();

Lines 5970-5980

  5970             case SQL_LONGVARBINARY:
  5971                 format = "Z";
  5972                 arrowColumnProducer->varVal =
  5973                     AllocateArrowArray<uint64_t>(offsetCount, reservedBytes,
! 5974                                                  "Arrow offset buffer is too large");
! 5975                 EnsureNativeFetchBufferSize(arrowColumnProducer->varData, initialVarDataSize,
! 5976                                         reservedBytes);
  5977                 columnVarLen[i] = true;
  5978                 // start at offset 0
  5979                 arrowColumnProducer->varVal[0] = 0;
  5980                 arrowColumnProducer->ptrValueBuffer = arrowColumnProducer->varVal.get();

Lines 6129-6137

  6129     SQLULEN& numRowsFetched = pending->count;
  6130     FetchStateGuard fetchStateGuard(StatementHandle, messages);
  6131 
  6132     if (!hasLobColumns && bufferFetchSize > 0) {
! 6133         const size_t before = reservedBytes;
  6134         ret = SQLBindColums(hStmt, buffers, columnNames, numCols, fetchSize, reservedBytes,
  6135                             charCtype, messages);
  6136         pending->reservedBytes += reservedBytes - before;
  6137         if (!SQL_SUCCEEDED(ret)) {

Lines 6194-6208

  6194                     const SQLLEN indicator = buffers.indicators[col][idxRowSql];
  6195                     if (indicator < 0) {
  6196                         ThrowStdException("Unexpected negative data length.");
  6197                     }
! 6198                     size_t bytes = static_cast<size_t>(indicator);
! 6199                     const auto type = dataTypes[col];
! 6200                     if (type == SQL_GUID) {
! 6201                         bytes = 37;
! 6202                     } else if (type == SQL_WCHAR || type == SQL_WVARCHAR ||
! 6203                                type == SQL_WLONGVARCHAR || type == SQL_SS_XML ||
! 6204                                ((type == SQL_CHAR || type == SQL_VARCHAR ||
  6205                                  type == SQL_LONGVARCHAR) && charCtype == SQL_C_WCHAR)) {
  6206                         if (bytes % sizeof(SQLWCHAR) != 0) {
  6207                             ThrowStdException("Wide-character data has an invalid byte length");
  6208                         }

Lines 6289-6297

  6289                                                             ? buffers.charBuffers[idxCol].size()
  6290                                                             : buffers.charBuffers[idxCol].size() /
  6291                                                                   static_cast<size_t>(fetchSize);
  6292                             const size_t sourceOffset = CheckedArrowSourceOffset(
! 6293                                 buffers.charBuffers[idxCol], idxRowSql, sourceStride, dataLen);
  6294 
  6295                             std::memcpy(&(*target_vec)[start],
  6296                                         &buffers.charBuffers[idxCol][sourceOffset], dataLen);
  6297                         }

mssql_python/pybind/ddbc_bindings.h

  346     }
  347     bool publishPendingArrowRows(uint64_t generation, std::shared_ptr<PendingArrowRows> rows) {
  348         std::lock_guard<std::mutex> lock(_pendingRowsMutex);
  349         if (generation != _pendingRowsGeneration) {
! 350             return false;
! 351         }
  352         _pendingArrowRows = std::move(rows);
  353         return true;
  354     }
  355     void setPendingArrowRows(std::shared_ptr<PendingArrowRows> rows) {

mssql_python/pybind/param_detect.hpp

Lines 141-158

  141 inline constexpr int MAX_INLINE_BINARY = 8000;
  142 
  143 inline SQLULEN DAEColumnSize(SQLSMALLINT sqlType, SQLULEN actualSize) {
  144     switch (sqlType) {
! 145         case SQL_CHAR:
  146         case SQL_VARCHAR:
  147             return actualSize > MAX_INLINE_CHAR ? 0 : actualSize;
! 148         case SQL_WCHAR:
  149         case SQL_WVARCHAR:
  150             return actualSize > MAX_INLINE_CHAR ? 0 : actualSize;
! 151         case SQL_BINARY:
  152         case SQL_VARBINARY:
  153             return actualSize > MAX_INLINE_BINARY ? 0 : actualSize;
! 154         case SQL_LONGVARCHAR:
  155         case SQL_WLONGVARCHAR:
  156         case SQL_LONGVARBINARY:
  157             return actualSize;
  158         default:

Lines 237-252

  237         if (!chunk) throw py::error_already_set();
  238         py::object encoded = encoder.attr("encode")(chunk, end == length);
  239         const Py_ssize_t encodedSize = PyBytes_GET_SIZE(encoded.ptr());
  240         if (encodedSize > MAX_INLINE_BINARY - total) {
! 241             return MAX_INLINE_BINARY + 1;
! 242         }
  243         total += encodedSize;
  244     }
  245     if (length == 0) {
! 246         py::object encoded = encoder.attr("encode")(py::str(), true);
! 247         total = PyBytes_GET_SIZE(encoded.ptr());
! 248     }
  249     return total;
  250 }
  251 
  252 inline PyObject* FormatDecimalParam(PyObject* params, Py_ssize_t index, PyObject* value) {

Lines 275-290

  275 }
  276 
  277 inline long long ValidatedInputSizeInteger(PyObject* value, const char* fieldName) {
  278     if (!PyLong_Check(value) || PyBool_Check(value)) {
! 279         throw py::type_error(std::string(fieldName) + " must be an integer");
! 280     }
  281     int overflow = 0;
  282     const long long result = PyLong_AsLongLongAndOverflow(value, &overflow);
  283     if ((result == -1 && PyErr_Occurred()) || overflow != 0) {
! 284         PyErr_Clear();
! 285         throw py::value_error(std::string(fieldName) + " is out of range");
! 286     }
  287     return result;
  288 }
  289 
  290 inline void ValidateInputSizes(PyObject* inputSizes) {

Lines 316-325

  316         if (sqlType < std::numeric_limits<SQLSMALLINT>::min() ||
  317             sqlType > std::numeric_limits<SQLSMALLINT>::max() ||
  318             cType < std::numeric_limits<SQLSMALLINT>::min() ||
  319             cType > std::numeric_limits<SQLSMALLINT>::max()) {
! 320             throw py::value_error("SQL and C types must fit in SQLSMALLINT");
! 321         }
  322         const bool isNumeric = sqlType == SQL_DECIMAL || sqlType == SQL_NUMERIC;
  323         py::int_ zero(0);
  324         const int negativeColumnSize =
  325             PyObject_RichCompareBool(columnSize, zero.ptr(), Py_LT);

Lines 325-340

  325             PyObject_RichCompareBool(columnSize, zero.ptr(), Py_LT);
  326         const int negativeDecimalDigits =
  327             PyObject_RichCompareBool(decimalDigits, zero.ptr(), Py_LT);
  328         if (negativeColumnSize == -1 || negativeDecimalDigits == -1) {
! 329             throw py::error_already_set();
! 330         }
  331         if (negativeColumnSize == 1) {
  332             throw py::value_error("column size must be non-negative");
  333         }
  334         if (negativeDecimalDigits == 1) {
! 335             throw py::value_error("decimal digits must be non-negative");
! 336         }
  337         if (!isNumeric) {
  338             const unsigned long long requestedSize = PyLong_AsUnsignedLongLong(columnSize);
  339             if (requestedSize == static_cast<unsigned long long>(-1) && PyErr_Occurred()) {
  340                 PyErr_Clear();

Lines 340-349

  340                 PyErr_Clear();
  341                 throw py::value_error("column size is out of range");
  342             }
  343             if (requestedSize > std::numeric_limits<SQLULEN>::max()) {
! 344                 throw py::value_error("column size is out of range");
! 345             }
  346             const unsigned long long requestedDigits =
  347                 PyLong_AsUnsignedLongLong(decimalDigits);
  348             if (requestedDigits == static_cast<unsigned long long>(-1) && PyErr_Occurred()) {
  349                 PyErr_Clear();

Lines 350-359

  350                 throw py::value_error("decimal digits are out of range");
  351             }
  352             if (requestedDigits >
  353                 static_cast<unsigned long long>(std::numeric_limits<SQLSMALLINT>::max())) {
! 354                 throw py::value_error("decimal digits are out of range");
! 355             }
  356         }
  357     }
  358 }

Lines 477-486

  477                                                const std::string& charEncoding = "utf-8") {
  478     PyTypeCache::initialize();
  479 
  480     if (!PyList_Check(params)) {
! 481         throw py::type_error("params must be a list");
! 482     }
  483     ValidateInputSizes(inputSizes);
  484 
  485     const Py_ssize_t n = PyList_GET_SIZE(params);
  486     const Py_ssize_t inputSizeCount = inputSizes == Py_None ? 0 : PyList_GET_SIZE(inputSizes);


📋 Files Needing Attention

📉 Files with overall lowest coverage (click to expand)
mssql_python.pybind.performance_counter.hpp: 0.7%
mssql_python.pybind.logger_bridge.cpp: 57.9%
mssql_python.pybind.ddbc_bindings.h: 65.6%
mssql_python.pybind.logger_bridge.hpp: 70.8%
mssql_python.pybind.connection.connection_pool.cpp: 82.3%
mssql_python.pybind.ddbc_bindings.cpp: 82.3%
mssql_python.pybind.connection.connection.cpp: 83.2%
mssql_python.logging.py: 86.2%
mssql_python.pooling.py: 90.1%
mssql_python.pybind.param_detect.hpp: 90.2%

🔗 Quick Links

⚙️ Build Summary 📋 Coverage Details

View Azure DevOps Build

Browse Full Coverage Report

Copilot AI review requested due to automatic review settings September 21, 2026 08:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

Binary input validation must also cover array bindings consistently.

Review effort: Lite
Findings: 4 High severity · 1 Medium severity

Open (5)

@github-actions github-actions Bot added the pr-size: medium Moderate update size label Sep 21, 2026
Copilot AI review requested due to automatic review settings September 21, 2026 09:25

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

arrow_reader() should validate batch_size before fetching the schema and performing native work.

Review effort: Lite
Findings: None

Resolved since last review (5)

Copilot AI review requested due to automatic review settings September 22, 2026 05:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Critical native buffer-copy and allocation risks, plus incomplete public batch-size validation, remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 2 High severity

Open (2)

Comment thread mssql_python/cursor.py
Comment thread mssql_python/pybind/ddbc_bindings.cpp Outdated
Copilot AI review requested due to automatic review settings September 22, 2026 05:42
@github-actions github-actions Bot added pr-size: large Substantial code update and removed pr-size: medium Moderate update size labels Sep 22, 2026
Comment thread mssql_python/pybind/ddbc_bindings.cpp Outdated
@subrata-ms

Copy link
Copy Markdown
Contributor

gargsaumya , Can we improve the code coverage?
Otherwise, change looks good to me.

Reject boolean, zero, oversized chunk counts and invalid chunk sizes before the internal production test binding can enter its simulation loop.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Wide-character binary validation and the direct <limits> include remain unresolved, and native DAE coverage is incomplete.

0 open findings

🧠 Review effort: Lite


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Unresolved sizing gaps remain in the native binding paths, and the broad ODBC changes need live cross-platform validation.

2 open findings

🧠 Review effort: Lite

Comment thread mssql_python/pybind/param_detect.hpp
Comment thread mssql_python/cursor.py
gargsaumya and others added 2 commits October 12, 2026 12:17
Normalize time values before DAE sizing and scan automatic executemany text columns by UTF-16 code units.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The broad native binding and fetch changes need human review and cross-platform runtime validation, and a debug-path safety issue remains.

3 open findings

🧠 Review effort: Lite

Comment thread mssql_python/pybind/ddbc_bindings.cpp
Avoid formatting an indeterminate SQLParamData token when the driver reports completion without writing it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Numeric overrides can still bypass the allocation limit, and streamed batches can stop before execution completes.

2 open findings
3 resolved since last review

🧠 Review effort: Lite

Comment thread mssql_python/pybind/ddbc_bindings.cpp Outdated
Comment thread mssql_python/pybind/param_detect.hpp
Continue SQLParamData after each streamed executemany parameter and reject oversized numeric override text before native copying.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The native binding and fetch changes span several high-risk paths, and the remaining issues need resolution and cross-platform runtime validation.

1 open finding
2 resolved since last review
Previously missed (2)

In code that hasn't changed since last review

Medium severity Batch size scan misses normalized datetime.time text lengths

mssql_python/​cursor.py:2652

The executemany size scan only includes existing str values, but text-bound datetime.time values are converted with isoformat() later, in the processed_parameters loop (cursor.py:2844-2847). With setinputsizes([(SQL_WVARCHAR, 1, 0)]) and a time subclass whose isoformat() returns more than 4,000 UTF-16 units, isDAE stays false; the array binder rejects the normalized value against the declared size instead of streaming it. Measure the normalized text before deciding on array binding, as scalar execute already does, and add a batch regression test.

Medium severity Synchronous fetchmany rejects non-positive sizes inconsistently

mssql_python/​cursor.py:3040

Validating size here makes synchronous fetchmany(-1) raise ValueError, where it previously returned [] before reaching native code. AsyncCursor.fetchmany(-1) still returns [] (async_query/async_fetch.py:91-95), and its existing test asserts this (tests/AsyncTest/test_007_async_fetch.py:654-655). Preserve the synchronous non-positive short-circuit if that is the intended API contract, or update both cursor APIs and their tests together; rejecting a value that never crosses the native boundary need not create this inconsistency.

🧠 Review effort: Lite

Comment thread mssql_python/pybind/param_detect.hpp
Encode string subclasses from their immutable Unicode contents so overridden encode methods cannot bypass native sizing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Broad native binding and streaming changes need cross-platform runtime review, and automatic time normalization still has an unresolved size gap.

0 open findings

1 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity Oversized datetime.time values bypass streaming detection

mssql_python/​pybind/​param_detect.hpp:620

The new size-based streaming decision only runs for values that arrive as str. Without setinputsizes, DetectParamTypes handles a datetime.time separately: it calls isoformat() and returns without measuring the resulting text. A time subclass whose isoformat() returns a very large string therefore leaves isDAE false, and BindParameters copies that string into an unbounded native UTF-16 buffer. Check the normalized value in the automatic time path too, rejecting oversized text if SQL_TYPE_TIME cannot be streamed safely. The new large-time test covers only an explicit text override.

🧠 Review effort: Lite

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Two executemany sizing issues remain, and the broad native streaming changes need cross-platform runtime review.

1 open finding
Previously missed (1)

In code that hasn't changed since last review

Medium severity Time subclass conversion bypasses executemany size detection

mssql_python/​cursor.py:2670

With setinputsizes([(SQL_VARCHAR, 1, 0)]), an executemany row containing a datetime.time subclass whose isoformat() returns 4,001 characters is not included in this str-only scan. The later conversion turns it into text, but array binding has already been selected and rejects its length against the declared size; the same value works through scalar execute, which normalizes before choosing streaming. Normalize and size time values before selecting array binding, or route such rows through scalar execution.

🧠 Review effort: Lite

Comment thread mssql_python/cursor.py Outdated
Bypass string subclass protocol overrides during bounded narrow and UTF-16 executemany sizing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The broad native binding and fetch changes need cross-platform human validation, and a binary-subclass sizing issue remains unresolved.

0 open findings

1 resolved since last review
Previously missed (1)

In code that hasn't changed since last review

Medium severity Use actual buffer length instead of overridable len() for DAE selection

mssql_python/​cursor.py:2679

Both the new explicit-size scan here and the automatic binary scan at cursor.py:2808-2815 use overridable len(value). A bytes or bytearray subclass can report length 1 while holding 8,001 bytes. executemany() then leaves DAE disabled; the native array binder reads the real buffer size and rejects the row as exceeding its allocated column size, although the scalar path can stream it. Measure the underlying buffer length in both scans so the actual payload selects streaming.

🧠 Review effort: Lite

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The broad native allocation and streaming changes need cross-platform builds and live SQL Server validation before approval.

0 open findings

🧠 Review effort: Lite

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Moderate issues remain in executemany sizing and wide-schema Arrow preallocation.

2 open findings

🧠 Review effort: Lite

Comment thread mssql_python/cursor.py
Comment thread mssql_python/cursor.py

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revalidated the four previously drafted comments against bf5e738. The row/tuple comparison assertion is fixed, so I have omitted that comment. This is a targeted revalidation of those comments, not a new full review.

Document the compatibility changes

Please document the new 1,000,000-row caps (including bulk-copy rows per commit), the native allocation budgets, and changed negative/boolean-size behavior in API documentation and the changelog. Explain how parameter-array limits depend on batch size and column widths, with guidance for reducing batch sizes. Distinguish native buffer limits from total process or result memory.

Comment thread mssql_python/pybind/ddbc_bindings.cpp
Comment thread tests/test_004_cursor.py Outdated
Reject oversized formatted numeric batch parameters before native binding. Cover the rejection boundary and the existing wide automatic text binding, and document row caps, allocation budgets, and compatibility changes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread tests/test_023_execute_path_parity.py
Comment thread README.md

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Unresolved moderate issues remain in binary sizing, effective text encoding, and automatic wide-text binding.

No open issues 🎉

2 resolved since last review

Review effort: Lite

Use dedicated context-managed cursors so expected binding exceptions cannot leave input-size hints on the shared module cursor. Verified the reported binary-rejection then numeric-test ordering: eight cases pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Three unresolved moderate validation and allocation issues remain.

No open issues 🎉

Review effort: Lite

Use budget-aware amortized growth and lossless partial Arrow batches with statement-owned pending rows and resumable LOB reads. Preserve pending state across mixed fetch, raw fetch/GetData, scroll/skip, and metadata-only attribute changes; clear it on real result invalidation. Document short-batch semantics and add isolated ordinary/LOB and lifecycle regressions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Three moderate issues remain in row-count accounting and native buffer-budget calculations.

No open issues 🎉

Review effort: Lite

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-size: large Substantial code update

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants