Skip to content

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

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

gargsaumya wants to merge 34 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
256 KiB VARCHAR(MAX) / fetchall() 103.2% slower 79.7% 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 256 KiB VARCHAR(MAX) / fetchall() 1.228 ms 2.461 ms +103.2%
Unix / SQL Server 2025 256 KiB VARCHAR(MAX) / fetchall() 1.525 ms 2.753 ms +79.7%
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

256 KiB VARCHAR(MAX) / fetchall(): ddbc::FetchAll_wrap +1.231 ms; py::fetchall::cpp_call +1.231 ms; ddbc::SQLGetData_wrap +1.229 ms.

Unix / SQL Server 2025

256 KiB VARCHAR(MAX) / fetchall(): ddbc::FetchLobColumnData +1.245 ms; ddbc::SQLGetData_wrap +1.244 ms; ddbc::FetchAll_wrap +1.235 ms.

All database tasks and timings

Unix / SQL Server 2022

Database task Before After Paired change Result
Connection opening 10.382 ms 10.247 ms -1.3% no signal
SELECT queries 1.070 ms 1.106 ms +0.8% no signal
Row insertion 34.324 ms 34.906 ms -0.1% no signal
Executemany inserts 157.475 ms 173.673 ms +10.1% no signal
Fetch-all queries 121.321 ms 120.789 ms -0.0% no signal
Row-by-row fetching 14.311 ms 14.218 ms -1.6% no signal
Batched row fetching 115.740 ms 117.942 ms +1.7% no signal
Transaction commit and rollback 114.372 ms 114.946 ms +0.5% no signal
Arrow row fetching 94.264 ms 96.084 ms +1.9% no signal
100,000-row insertion 463.502 ms 454.789 ms -2.3% no signal
Row fetching in batches of 100 121.941 ms 123.889 ms +0.2% no signal
Row fetching in batches of 10,000 131.885 ms 140.823 ms +9.8% no signal
Repeated positional queries 33.673 ms 33.896 ms +0.0% no signal
Repeated named-parameter queries 35.801 ms 36.712 ms +1.7% no signal
Legacy 100,000-row insertion 353.350 ms 358.110 ms +1.3% no signal
Insertion with explicit input sizes 502.255 ms 592.667 ms +17.9% no signal
Joined aggregation queries 181.330 ms 184.856 ms -0.3% no signal
Large joined-result fetching 189.726 ms 197.129 ms +2.5% no signal
1.2-million-row fetching 3501.123 ms 3475.212 ms -1.0% no signal
Common table expression queries 5.313 ms 5.331 ms -0.4% no signal
256 KiB VARCHAR(MAX) / fetchall() 1.228 ms 2.461 ms +103.2% consistent slowdown
10,000 scalar values / fetchval() (debug disabled) 106.937 ms 108.565 ms +2.4% no signal

Unix / SQL Server 2025

Database task Before After Paired change Result
Connection opening 97.502 ms 97.097 ms -0.4% no signal
SELECT queries 1.141 ms 1.132 ms -2.6% no signal
Row insertion 35.550 ms 35.638 ms +3.1% no signal
Executemany inserts 160.061 ms 165.116 ms +9.2% no signal
Fetch-all queries 123.143 ms 125.260 ms +0.7% no signal
Row-by-row fetching 14.453 ms 14.636 ms -1.7% no signal
Batched row fetching 120.519 ms 120.792 ms -0.0% no signal
Transaction commit and rollback 116.607 ms 117.150 ms +0.9% no signal
Arrow row fetching 95.782 ms 97.453 ms +1.1% no signal
100,000-row insertion 459.060 ms 455.400 ms +2.9% no signal
Row fetching in batches of 100 123.715 ms 125.341 ms +0.6% no signal
Row fetching in batches of 10,000 132.998 ms 146.219 ms +9.9% no signal
Repeated positional queries 33.983 ms 34.256 ms -0.5% no signal
Repeated named-parameter queries 36.478 ms 36.639 ms +0.6% no signal
Legacy 100,000-row insertion 399.570 ms 374.319 ms -6.3% no signal
Insertion with explicit input sizes 501.446 ms 609.312 ms +19.6% no signal
Joined aggregation queries 160.034 ms 159.658 ms -0.6% no signal
Large joined-result fetching 194.861 ms 190.231 ms -3.4% no signal
1.2-million-row fetching 3559.067 ms 3621.418 ms +0.3% no signal
Common table expression queries 5.414 ms 5.296 ms +0.1% no signal
256 KiB VARCHAR(MAX) / fetchall() 1.525 ms 2.753 ms +79.7% consistent slowdown
10,000 scalar values / fetchval() (debug disabled) 108.381 ms 109.237 ms -0.3% no signal
Build and measurement details

ADO build 181147

PR head: f53323085fcf86a70bc68804b17e8b031a5818b5
Base: 666f3cb6d23981bb23cd182ec273df10a7b2c805
Measured merge: 5333a64288907cdafc76f05765a432b389c9999a

  • Unix / SQL Server 2022: Python 3.12.3, x86_64, SQL 16.0.4295.3; 5 paired comparisons and 1 warmup.
  • Unix / SQL Server 2025: Python 3.12.3, x86_64, SQL 17.0.5005.3; 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

84%


🎯 Overall Coverage

85%


📈 Total Lines Covered: 9958 out of 11664
📁 Project: mssql-python


Diff Coverage

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

  • mssql_python/cursor.py (78.1%): Missing lines 62-72,2657,2678,2706
  • mssql_python/pybind/ddbc_bindings.cpp (83.9%): Missing lines 329-330,339-340,346-347,354-355,363-364,419-421,431-432,437-438,443-444,446,458-459,466-467,520,563-564,571-579,587-589,599-603,617-618,625-626,2526-2529,2600,2602-2603,2654,2725-2729,2741,2758-2759,2792-2793,2797,2801-2802,2809,3312-3313,3371-3376,4683,4705,4726,4732,4758,4760,4803,5440,5580,5610,5672,5687-5688,5701-5702,5716-5717,5741-5743,5755-5756,6151
  • mssql_python/pybind/param_detect.hpp (90.8%): Missing lines 145,148,151,154,241-242,246-248

Summary

  • Total: 788 lines
  • Missing: 124 lines
  • Coverage: 84%

mssql_python/cursor.py

Lines 58-76

  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     for offset in range(0, len(value), chunk_size):
! 66         end = min(offset + chunk_size, len(value))
! 67         total += len(encoder.encode(value[offset:end], final=end == len(value)))
! 68         if total > limit:
! 69             return True
! 70     if not value:
! 71         total += len(encoder.encode("", final=True))
! 72     return total > limit
  73 
  74 
  75 # SQL BIGINT is a signed 64-bit integer. Ints outside this range have no BIGINT
  76 # encoding and must be rejected at detect time on both paths (see _map_sql_type).

Lines 2653-2661

  2653                         binary_values = [
  2654                             value for value in column if isinstance(value, (bytes, bytearray))
  2655                         ]
  2656                         if c_type == ddbc_sql_const.SQL_CHAR.value:
! 2657                             text_is_large = any(
  2658                                 _encoded_length_exceeds(
  2659                                     value,
  2660                                     encoding_settings["encoding"],
  2661                                     MAX_INLINE_CHAR,

Lines 2674-2682

  2674                         requires_row_fallback = bool(
  2675                             narrow_text_type and text_values and binary_values
  2676                         )
  2677                         if narrow_text_type and binary_values and not text_values:
! 2678                             c_type = ddbc_sql_const.SQL_CHAR.value
  2679                         is_dae = text_is_large or binary_is_large
  2680 
  2681                     # Sanitize precision/scale for numeric types
  2682                     if sql_type in (

Lines 2702-2710

  2702 
  2703                         # For SQL Server VARBINARY(MAX), we need to use large object binding
  2704                         if max_binary_size > MAX_INLINE_BINARY:
  2705                             if sql_type != ddbc_sql_const.SQL_SS_UDT.value:
! 2706                                 sql_type = ddbc_sql_const.SQL_LONGVARBINARY.value
  2707                             is_dae = True
  2708 
  2709                         # Update column_size to actual maximum size if it's larger
  2710                         # 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 415-425

  415         case SQL_C_CHAR:
  416         case SQL_C_BINARY:
  417             return CheckedAddSize(info.columnSize, 1, "Character parameter size is too large");
  418         case SQL_C_BIT:
! 419             return sizeof(char);
! 420         case SQL_C_STINYINT:
! 421         case SQL_C_USHORT:
  422             return sizeof(unsigned short);
  423         case SQL_C_SBIGINT:
  424         case SQL_C_SLONG:
  425         case SQL_C_UBIGINT:

Lines 427-450

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

Lines 454-463

  454         CheckedMultiplySize(rowIndex, rowStride, "Arrow source offset is too large");
  455     const size_t rowCapacity = CheckedMultiplySize(
  456         rowStride, sizeof(ElementType), "Arrow source capacity is too large");
  457     if (offset > buffer.size() || rowStride > buffer.size() - offset ||
! 458         dataBytes > rowCapacity) {
! 459         ThrowStdException("Driver data length exceeds the allocated fetch buffer");
  460     }
  461     return offset;
  462 }

Lines 462-471

  462 }
  463 
  464 template <typename ElementType>
  465 std::unique_ptr<ElementType[]> AllocateUniqueArray(size_t count, const char* errorMessage) {
! 466     if (count > std::numeric_limits<size_t>::max() / sizeof(ElementType)) {
! 467         ThrowStdException(errorMessage);
  468     }
  469     return std::make_unique<ElementType[]>(count);
  470 }

Lines 516-524

  516                                                   const char* errorMessage) {
  517     ReserveNativeFetchBytes(reservedBytes, count, sizeof(ElementType));
  518     return AllocateUniqueArray<ElementType>(count, errorMessage);
  519 }
! 520 
  521 std::string DescribeChar(unsigned char ch) {
  522     if (ch >= 32 && ch <= 126) {
  523         return std::string("'") + static_cast<char>(ch) + "'";
  524     } else {

Lines 559-568

  559         if (!chunk) throw py::error_already_set();
  560         py::object encoded = encoder.attr("encode")(chunk, end == length);
  561         char* data = nullptr;
  562         Py_ssize_t size = 0;
! 563         if (PyBytes_AsStringAndSize(encoded.ptr(), &data, &size) != 0) {
! 564             throw py::error_already_set();
  565         }
  566         if (size > 0) {
  567             SQLRETURN rc = put_data_fn(data, static_cast<SQLLEN>(size));
  568             if (!SQL_SUCCEEDED(rc)) return rc;

Lines 567-583

  567             SQLRETURN rc = put_data_fn(data, static_cast<SQLLEN>(size));
  568             if (!SQL_SUCCEEDED(rc)) return rc;
  569         }
  570     }
! 571     if (length == 0) {
! 572         py::object encoded = encoder.attr("encode")(py::str(), true);
! 573         char* data = nullptr;
! 574         Py_ssize_t size = 0;
! 575         if (PyBytes_AsStringAndSize(encoded.ptr(), &data, &size) != 0) {
! 576             throw py::error_already_set();
! 577         }
! 578         if (size > 0) return put_data_fn(data, static_cast<SQLLEN>(size));
! 579         return put_data_fn(nullptr, 0);
  580     }
  581     return SQL_SUCCESS;
  582 }

Lines 583-593

  583 
  584 static SQLRETURN StreamDAEParameter(SQLHSTMT hStmt, const ParamInfo& info,
  585                                     const std::string& charEncoding) {
  586     PyObject* value = info.dataPtr.ptr();
! 587     if (!value || value == Py_None) {
! 588         py::gil_scoped_release release;
! 589         return SQLPutData_ptr(hStmt, nullptr, 0);
  590     }
  591 
  592     auto putImmutableData = [&](SQLPOINTER data, SQLLEN length) {
  593         py::gil_scoped_release release;

Lines 595-607

  595     };
  596     if (PyUnicode_Check(value)) {
  597         if (info.paramCType == SQL_C_WCHAR) {
  598             return stream_unicode_dae_chunks(value, "utf-16-le", putImmutableData);
! 599         }
! 600         if (info.paramCType == SQL_C_CHAR) {
! 601             return stream_unicode_dae_chunks(value, charEncoding, putImmutableData);
! 602         }
! 603         ThrowStdException("DAE only supports text C types for str values");
  604     }
  605     if (PyBytes_Check(value)) {
  606         return stream_dae_chunks(PyBytes_AS_STRING(value),
  607                                  static_cast<size_t>(PyBytes_GET_SIZE(value)),

Lines 613-622

  613         std::vector<char> chunk(std::min(static_cast<size_t>(DAE_CHUNK_SIZE), totalBytes));
  614         for (size_t offset = 0; offset < totalBytes; offset += chunk.size()) {
  615             const size_t currentSize = static_cast<size_t>(PyByteArray_GET_SIZE(value));
  616             const size_t length = std::min(chunk.size(), totalBytes - offset);
! 617             if (currentSize < offset + length) {
! 618                 ThrowStdException("bytearray changed size during DAE streaming");
  619             }
  620             std::copy_n(PyByteArray_AS_STRING(value) + offset, length, chunk.data());
  621             SQLRETURN rc = putImmutableData(chunk.data(), static_cast<SQLLEN>(length));
  622             if (!SQL_SUCCEEDED(rc)) return rc;

Lines 621-630

  621             SQLRETURN rc = putImmutableData(chunk.data(), static_cast<SQLLEN>(length));
  622             if (!SQL_SUCCEEDED(rc)) return rc;
  623         }
  624         return SQL_SUCCESS;
! 625     }
! 626     ThrowStdException("DAE only supports str, bytes, or bytearray values");
  627     return SQL_ERROR;
  628 }
  629 
  630 // GH-610: Resolve SQL type for a NULL parameter using per-handle cache.

Lines 2522-2533

  2522         SQLRETURN describeRc = PreResolveUdtTypes(hStmt, paramInfos);
  2523         if (!SQL_SUCCEEDED(describeRc)) return describeRc;
  2524         // GH-627: resolve unknown NULL array param SQL types before binding any param.
  2525         PreResolveUnknownNullTypes(handle, hStmt, paramInfos);
! 2526         size_t reservedParameterBytes = 0;
! 2527         for (const ParamInfo& info : paramInfos) {
! 2528             ReserveNativeParameterBytes(reservedParameterBytes, paramSetSize,
! 2529                                         ParameterArrayElementSize(info));
  2530             ReserveNativeParameterBytes(reservedParameterBytes, paramSetSize, sizeof(SQLLEN));
  2531         }
  2532         for (int paramIndex = 0; paramIndex < columnwise_params.size(); ++paramIndex) {
  2533             const py::list& columnValues = columnwise_params[paramIndex].cast<py::list>();

Lines 2596-2607

  2596                         elementWidth, sizeof(SQLWCHAR),
  2597                         "Wide-character parameter length is too large");
  2598                     if (bufferBytes > static_cast<size_t>(std::numeric_limits<SQLLEN>::max())) {
  2599                         ThrowStdException("Wide-character parameter length is too large");
! 2600                     }
  2601                     SQLWCHAR* wcharArray = AllocateParamBufferArray<SQLWCHAR>(
! 2602                         tempBuffers,
! 2603                         CheckedMultiplySize(paramSetSize, elementWidth,
  2604                                             "Wide-character parameter buffer is too large"));
  2605                     strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
  2606                     for (size_t i = 0; i < paramSetSize; ++i) {
  2607                         if (columnValues[i].is_none()) {

Lines 2650-2658

  2650                     LOG("BindParameterArray: SQL_C_WCHAR bound - "
  2651                         "param_index=%d",
  2652                         paramIndex);
  2653                     dataPtr = wcharArray;
! 2654                     bufferLength = static_cast<SQLLEN>(bufferBytes);
  2655                     break;
  2656                 }
  2657                 case SQL_C_TINYINT:
  2658                 case SQL_C_UTINYINT: {

Lines 2721-2733

  2721                 case SQL_C_BINARY: {
  2722                     LOG("BindParameterArray: Binding SQL_C_CHAR/BINARY array - "
  2723                         "param_index=%d, count=%zu, column_size=%zu, encoding='%s'",
  2724                         paramIndex, paramSetSize, info.columnSize, charEncoding.c_str());
! 2725                     const size_t elementWidth = CheckedAddSize(
! 2726                         info.columnSize, 1, "Character parameter size is too large");
! 2727                     if (elementWidth > static_cast<size_t>(std::numeric_limits<SQLLEN>::max())) {
! 2728                         ThrowStdException("Character parameter length is too large");
! 2729                     }
  2730                     char* charArray = AllocateParamBufferArray<char>(
  2731                         tempBuffers,
  2732                         CheckedMultiplySize(paramSetSize, elementWidth,
  2733                                             "Character parameter buffer is too large"));

Lines 2737-2745

  2737                             strLenOrIndArray[i] = SQL_NULL_DATA;
  2738                             std::memset(charArray + i * (info.columnSize + 1), 0,
  2739                                         info.columnSize + 1);
  2740                         } else {
! 2741                             if (info.paramCType == SQL_C_BINARY &&
  2742                                 !py::isinstance<py::bytes>(columnValues[i]) &&
  2743                                 !py::isinstance<py::bytearray>(columnValues[i])) {
  2744                                 ThrowStdException(MakeParamMismatchErrorStr(info.paramCType,
  2745                                                                             paramIndex));

Lines 2754-2763

  2754                                     encoded =
  2755                                         columnValues[i].attr("encode")(charEncoding, "strict");
  2756                                     if (PyBytes_AsStringAndSize(encoded.ptr(), &encodedData,
  2757                                                                 &encodedSize) != 0) {
! 2758                                         throw py::error_already_set();
! 2759                                     }
  2760                                     LOG("BindParameterArray: param[%d] row[%zu] SQL_C_CHAR - "
  2761                                         "Encoded with '%s', "
  2762                                         "size=%zu bytes",
  2763                                         paramIndex, i, charEncoding.c_str(),

Lines 2788-2806

  2788                                 ThrowStdException(
  2789                                     MakeParamMismatchErrorStr(info.paramCType, paramIndex));
  2790                             }
  2791 
! 2792                             const size_t dataSize = static_cast<size_t>(encodedSize);
! 2793                             if (dataSize > info.columnSize) {
  2794                                 LOG("BindParameterArray: String/binary too "
  2795                                     "long - param_index=%d, row=%zu, size=%zu, "
  2796                                     "max=%zu",
! 2797                                     paramIndex, i, dataSize, info.columnSize);
  2798                                 ThrowStdException("Input exceeds column size at index " +
  2799                                                   std::to_string(i));
  2800                             }
! 2801                             std::copy_n(encodedData, dataSize, charArray + i * elementWidth);
! 2802                             strLenOrIndArray[i] = static_cast<SQLLEN>(dataSize);
  2803                         }
  2804                     }
  2805                     LOG("BindParameterArray: SQL_C_CHAR/BINARY bound - "
  2806                         "param_index=%d",

Lines 2805-2813

  2805                     LOG("BindParameterArray: SQL_C_CHAR/BINARY bound - "
  2806                         "param_index=%d",
  2807                         paramIndex);
  2808                     dataPtr = charArray;
! 2809                     bufferLength = static_cast<SQLLEN>(elementWidth);
  2810                     break;
  2811                 }
  2812                 case SQL_C_BIT: {
  2813                     LOG("BindParameterArray: Binding SQL_C_BIT array - "

Lines 3308-3317

  3308 
  3309             std::vector<ParamInfo> rowParamInfos = paramInfos;
  3310             for (size_t paramIndex = 0; paramIndex < rowParamInfos.size(); ++paramIndex) {
  3311                 if (rowParams[paramIndex].is_none()) {
! 3312                     rowParamInfos[paramIndex].paramCType = SQL_C_DEFAULT;
! 3313                     rowParamInfos[paramIndex].isDAE = false;
  3314                     rowParamInfos[paramIndex].dataPtr = py::none();
  3315                 }
  3316             }
  3317             std::vector<std::shared_ptr<void>> paramBuffers;

Lines 3367-3380

  3367                 LOG("SQLExecuteMany: DAE row %zu failed - rc=%d", rowIndex, rc);
  3368                 return rc;
  3369             }
  3370 
! 3371             const SQLRETURN rowRc = rc;
! 3372             rc = SQLFreeStmt_ptr(hStmt, SQL_RESET_PARAMS);
! 3373             if (!SQL_SUCCEEDED(rc)) {
! 3374                 LOG("SQLExecuteMany: SQL_RESET_PARAMS failed for row %zu - "
! 3375                     "rc=%d",
! 3376                     rowIndex, rc);
  3377                 return rc;
  3378             }
  3379             rc = rowRc;
  3380         }

Lines 4679-4687

  4679                 ret = SQLBindCol_ptr(hStmt, col, SQL_C_TINYINT, buffers.charBuffers[col - 1].data(),
  4680                                      sizeof(SQLCHAR), buffers.indicators[col - 1].data());
  4681                 break;
  4682             case SQL_BIT:
! 4683                 ResizeNativeFetchBuffer(buffers.charBuffers[col - 1], fetchSize, reservedBytes);
  4684                 ret = SQLBindCol_ptr(hStmt, col, SQL_C_BIT, buffers.charBuffers[col - 1].data(),
  4685                                      sizeof(SQLCHAR), buffers.indicators[col - 1].data());
  4686                 break;
  4687             case SQL_REAL:

Lines 4701-4709

  4701                                      buffers.indicators[col - 1].data());
  4702                 break;
  4703             case SQL_DOUBLE:
  4704             case SQL_FLOAT:
! 4705                 ResizeNativeFetchBuffer(buffers.doubleBuffers[col - 1], fetchSize, reservedBytes);
  4706                 ret =
  4707                     SQLBindCol_ptr(hStmt, col, SQL_C_DOUBLE, buffers.doubleBuffers[col - 1].data(),
  4708                                    sizeof(SQLDOUBLE), buffers.indicators[col - 1].data());
  4709                 break;

Lines 4722-4730

  4722                     SQLBindCol_ptr(hStmt, col, SQL_C_SBIGINT, buffers.bigIntBuffers[col - 1].data(),
  4723                                    sizeof(SQLBIGINT), buffers.indicators[col - 1].data());
  4724                 break;
  4725             case SQL_TYPE_DATE:
! 4726                 ResizeNativeFetchBuffer(buffers.dateBuffers[col - 1], fetchSize, reservedBytes);
  4727                 ret =
  4728                     SQLBindCol_ptr(hStmt, col, SQL_C_TYPE_DATE, buffers.dateBuffers[col - 1].data(),
  4729                                    sizeof(SQL_DATE_STRUCT), buffers.indicators[col - 1].data());
  4730                 break;

Lines 4728-4736

  4728                     SQLBindCol_ptr(hStmt, col, SQL_C_TYPE_DATE, buffers.dateBuffers[col - 1].data(),
  4729                                    sizeof(SQL_DATE_STRUCT), buffers.indicators[col - 1].data());
  4730                 break;
  4731             case SQL_SS_TIME2:
! 4732                 ResizeNativeFetchBuffer(buffers.timeBuffers[col - 1], fetchSize, reservedBytes);
  4733                 ret =
  4734                     SQLBindCol_ptr(hStmt, col, SQL_C_SS_TIME2, buffers.timeBuffers[col - 1].data(),
  4735                                    sizeof(SQL_SS_TIME2_STRUCT), buffers.indicators[col - 1].data());
  4736                 break;

Lines 4754-4764

  4754                 ret = SQLBindCol_ptr(hStmt, col, SQL_C_BINARY, buffers.charBuffers[col - 1].data(),
  4755                                      CheckedFetchBufferLength(fetchBufferSize, 1),
  4756                                      buffers.indicators[col - 1].data());
  4757                 break;
! 4758             }
  4759             case SQL_SS_TIMESTAMPOFFSET:
! 4760                 ResizeNativeFetchBuffer(buffers.datetimeoffsetBuffers[col - 1], fetchSize,
  4761                                         reservedBytes);
  4762                 ret = SQLBindCol_ptr(hStmt, col, SQL_C_SS_TIMESTAMPOFFSET,
  4763                                      buffers.datetimeoffsetBuffers[col - 1].data(),
  4764                                      sizeof(DateTimeOffset),

Lines 4799-4807

  4799     PERF_TIMER("FetchBatchData");
  4800     LOG("FetchBatchData: Fetching data in batches");
  4801     SQLRETURN ret;
  4802     {
! 4803         numRowsFetched = 0;
  4804         // Release the GIL during the blocking ODBC fetch
  4805         py::gil_scoped_release release;
  4806         PERF_TIMER("FetchBatchData::SQLFetchScroll_call");
  4807         ret = SQLFetchScroll_ptr(hStmt, SQL_FETCH_NEXT, 0);

Lines 5436-5444

  5436     FetchStateGuard fetchStateGuard(StatementHandle, messages);
  5437 
  5438     // Bind columns
  5439     ret = SQLBindColums(hStmt, buffers, columnNames, numCols, fetchSize, reservedBytes, charCtype,
! 5440                         messages);
  5441     if (!SQL_SUCCEEDED(ret)) {
  5442         LOG("FetchMany_wrap: Error when binding columns - SQLRETURN=%d", ret);
  5443         return ret;
  5444     }

Lines 5576-5584

  5576     const size_t offsetCount = CheckedAddSize(batchSize, 1, "Arrow batch size is too large");
  5577     const size_t initialVarDataSize =
  5578         CheckedMultiplySize(batchSize, 42, "Arrow batch size is too large");
  5579     const size_t bitmapSize =
! 5580         CheckedAddSize(batchSize, 7, "Arrow batch size is too large") / 8;
  5581     // Fetch narrow char data as SQL_C_CHAR if on Linux/macOS and configured by the user
  5582     charCtype = EffectiveCharCtypeForFetch(charCtype, "utf-8");
  5583 
  5584     // An overly large fetch size doesn't seem to help performance.

Lines 5606-5614

  5606     std::vector<SQLSMALLINT> dataTypes(numCols);
  5607     std::vector<bool> columnNullable(numCols);
  5608     std::vector<bool> columnVarLen(numCols, false);
  5609     std::vector<int64_t> nullCounts(numCols, 0);
! 5610     size_t reservedBytes = 0;
  5611 
  5612     std::vector<std::unique_ptr<ArrowArrayPrivateData>> arrowArrayPrivateData(numCols);
  5613     std::vector<std::unique_ptr<ArrowSchemaPrivateData>> arrowSchemaPrivateData(numCols);
  5614     for (SQLSMALLINT i = 0; i < numCols; i++) {

Lines 5668-5676

  5668                 arrowColumnProducer->varVal =
  5669                     AllocateArrowArray<uint64_t>(offsetCount, reservedBytes,
  5670                                                  "Arrow offset buffer is too large");
  5671                 ResizeNativeFetchBuffer(arrowColumnProducer->varData, initialVarDataSize,
! 5672                                         reservedBytes);
  5673                 columnVarLen[i] = true;
  5674                 // start at offset 0
  5675                 arrowColumnProducer->varVal[0] = 0;
  5676                 arrowColumnProducer->ptrValueBuffer = arrowColumnProducer->varVal.get();

Lines 5683-5692

  5683                 arrowColumnProducer->ptrValueBuffer = arrowColumnProducer->uint8Val.get();
  5684                 break;
  5685             case SQL_SMALLINT:
  5686                 format = "s";
! 5687                 arrowColumnProducer->int16Val =
! 5688                     AllocateArrowArray<int16_t>(batchSize, reservedBytes,
  5689                                                 "Arrow value buffer is too large");
  5690                 arrowColumnProducer->ptrValueBuffer = arrowColumnProducer->int16Val.get();
  5691                 break;
  5692             case SQL_INTEGER:

Lines 5697-5706

  5697                 arrowColumnProducer->ptrValueBuffer = arrowColumnProducer->int32Val.get();
  5698                 break;
  5699             case SQL_BIGINT:
  5700                 format = "l";
! 5701                 arrowColumnProducer->int64Val =
! 5702                     AllocateArrowArray<int64_t>(batchSize, reservedBytes,
  5703                                                 "Arrow value buffer is too large");
  5704                 arrowColumnProducer->ptrValueBuffer = arrowColumnProducer->int64Val.get();
  5705                 break;
  5706             case SQL_REAL:

Lines 5712-5721

  5712                 break;
  5713             case SQL_FLOAT:
  5714             case SQL_DOUBLE:
  5715                 format = "g";
! 5716                 arrowColumnProducer->float64Val =
! 5717                     AllocateArrowArray<double>(batchSize, reservedBytes,
  5718                                                "Arrow value buffer is too large");
  5719                 arrowColumnProducer->ptrValueBuffer = arrowColumnProducer->float64Val.get();
  5720                 break;
  5721             case SQL_DECIMAL:

Lines 5737-5747

  5737             case SQL_TIMESTAMP:
  5738             case SQL_TYPE_TIMESTAMP:
  5739             case SQL_DATETIME:
  5740                 format = "tsu:";
! 5741                 arrowColumnProducer->tsMicroVal =
! 5742                     AllocateArrowArray<int64_t>(batchSize, reservedBytes,
! 5743                                                 "Arrow value buffer is too large");
  5744                 arrowColumnProducer->ptrValueBuffer = arrowColumnProducer->tsMicroVal.get();
  5745                 break;
  5746             case SQL_SS_TIMESTAMPOFFSET:
  5747                 format = "tsu:+00:00";

Lines 5751-5760

  5751                 arrowColumnProducer->ptrValueBuffer = arrowColumnProducer->tsMicroVal.get();
  5752                 break;
  5753             case SQL_TYPE_DATE:
  5754                 format = "tdD";
! 5755                 arrowColumnProducer->dateVal =
! 5756                     AllocateArrowArray<int32_t>(batchSize, reservedBytes,
  5757                                                 "Arrow value buffer is too large");
  5758                 arrowColumnProducer->ptrValueBuffer = arrowColumnProducer->dateVal.get();
  5759                 break;
  5760             case SQL_SS_TIME2:

Lines 6147-6155

  6147                                                             ? buffers.charBuffers[idxCol].size()
  6148                                                             : buffers.charBuffers[idxCol].size() /
  6149                                                                   static_cast<size_t>(fetchSize);
  6150                             const size_t sourceOffset = CheckedArrowSourceOffset(
! 6151                                 buffers.charBuffers[idxCol], idxRowSql, sourceStride, dataLen);
  6152 
  6153                             std::memcpy(&(*target_vec)[start],
  6154                                         &buffers.charBuffers[idxCol][sourceOffset], dataLen);
  6155                         }

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) {


📋 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: 62.6%
mssql_python.pybind.logger_bridge.hpp: 70.8%
mssql_python.pybind.ddbc_bindings.cpp: 80.9%
mssql_python.pybind.connection.connection_pool.cpp: 82.3%
mssql_python.pybind.connection.connection.cpp: 83.1%
mssql_python.logging.py: 86.2%
mssql_python.pooling.py: 90.1%
mssql_python.pybind.param_detect.hpp: 91.9%

🔗 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

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

DAE streaming tokens are validated against the wrong parameter-info vector, causing streamed rows to be rejected.

Review effort: Lite
Findings: 1 High severity

Open (1)
Resolved since last review (2)

Comment thread mssql_python/pybind/ddbc_bindings.cpp Outdated
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings October 7, 2026 09:43

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

Three unresolved moderate findings affect DAE sizing and oversized parameter handling.

Review effort: Lite
Findings: None

Resolved since last review (1)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings October 7, 2026 09:57

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

Bounded UTF-8 sizing can under-report payloads above 4,001 bytes and must be corrected.

Review effort: Lite
Findings: None

Previously missed (1)

In code that hasn't changed since last review

Medium severity Fix UTF-8 length reporting beyond the 4,001-byte cap

mssql_python/​pybind/​param_detect.hpp:241

EncodedUnicodeLength stops counting at 4,001 bytes, but DAEColumnSize uses that result to describe SQL_C_CHAR parameters. Any encoded string longer than 4,001 bytes is therefore advertised with a SQL_VARCHAR column size of 4,001; values between 4,002 and 8,000 are under-described, and values over 8,000 do not get the required MAX sentinel. For example, "\u0800" * 3000 encodes to 9,000 UTF-8 bytes while this probe returns 4,001, so the DAE payload metadata can reject or truncate the value. Continue the bounded measurement through the 8,000-byte boundary and return a value above that boundary when it is exceeded.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings October 7, 2026 10:10

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

Four moderate issues remain unresolved in DAE detection and native allocation budgeting.

Review effort: Lite
Findings: None

Use the Unicode-safe MAX boundary for VARCHAR data-at-execution metadata and compare the wide fetch regression by row values rather than generated Row identity.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings October 7, 2026 10:23

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 critical native validation and buffer-safety issues remain.

Review effort: Lite
Findings: 2 High severity

Open (2)

Comment thread mssql_python/pybind/ddbc_bindings.cpp
Comment thread mssql_python/pybind/ddbc_bindings.cpp Outdated
Prevent a too-small setinputsizes declaration from truncating later non-DAE text or binary rows after executemany falls back to row execution.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings October 7, 2026 10:41

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

Five unresolved findings remain, including one critical input-validation issue and four moderate safety issues.

Review effort: Lite
Findings: 3 High severity

Open (3)

Comment thread mssql_python/pybind/ddbc_bindings.cpp Outdated
Reject boolean row counts before conversion, guard empty Arrow character copies, validate bytearray inputs before C API access, and make the DAE regressions compare row values reliably.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings October 7, 2026 11:01
Comment thread mssql_python/pybind/ddbc_bindings.cpp

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 critical DAE storage lifetime and moderate Decimal and executemany validation issues block approval.

Review effort: Lite
Findings: 1 High severity

Open (1)
Resolved since last review (3)

Comment thread mssql_python/pybind/ddbc_bindings.cpp
Release ODBC parameter bindings before row-local buffers and SQLParamData tokens leave scope, and surface reset failures without disturbing execution diagnostics on error paths.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings October 7, 2026 11:16

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

Three moderate issues remain in encoding parity, LOB growth, and wide-LOB memory accounting.

Review effort: Lite
Findings: None

Resolved since last review (1)

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.

6 participants