Skip to content

PERF: Native C++ parameter detection and execute pipeline - #549

Merged
Gaurav Sharma (bewithgaurav) merged 51 commits into
mainfrom
bewithgaurav/insertmany-perf-detect-types
Aug 14, 2026
Merged

PERF: Native C++ parameter detection and execute pipeline#549
Gaurav Sharma (bewithgaurav) merged 51 commits into
mainfrom
bewithgaurav/insertmany-perf-detect-types

Conversation

@bewithgaurav

@bewithgauravGaurav Sharma (bewithgaurav) commented Apr 29, 2026

Copy link
Copy Markdown
Collaborator

Work Item / Issue Reference

AB#44979
AB#49196

GitHub Issue: #500


Summary

Moves parameter type detection and binding from Python into a native C++ pipeline using raw CPython API calls. The new DDBCSQLExecute handles type detection → parameter binding → SQLExecute in a single FFI crossing, eliminating per-parameter Python overhead entirely.

What changed:

  • DetectParamTypes — C++ type detection using raw CPython API (PyLong_Check, PyDateTime_Check, PyObject_RichCompareBool, etc.) replacing the Python-side _create_parameter_types_list loop for the standard execute path.
  • DDBCSQLExecute (formerly DDBCSQLExecuteFast) — single C++ pipeline: detect → bind → execute. ParamInfo never crosses the pybind11 boundary.
  • DDBCSQLExecuteLegacy (formerly DDBCSQLExecute) — retained for setinputsizes users only, and annotated in-code as slated for removal once those overrides are handled natively.
  • MONEY/SMALLMONEY bounds cached once at init using exact Decimal comparison (not lossy double).
  • PyTypeCache (py_type_cache.hpp) stores all type objects as raw PyObject* (not py::object), eliminating pybind11 wrapper overhead on every cache hit.
  • NUMERIC mantissa built from four fixed-width uint32 limbs instead of walking every Decimal digit through PyNumber_Multiply/PyNumber_Add. SQL Server caps NUMERIC at 38 digits, so the mantissa always fits 128 bits. Decimal detection measured 2.3–2.9x faster on its own.
  • Reference ownership is RAII throughout. ParamInfo::dataPtr holds a py::object rather than a raw PyObject* with a hand-written rule of five, and py_ref.hpp provides steal/borrow so every adoption of a CPython reference states whether it is taking a new reference or borrowing one.
  • Parameter detection lives in param_detect.hpp rather than inline in the 6600-line ddbc_bindings.cpp. Header-only because the build is -O3 with no LTO, so a .cpp boundary would also be an inlining boundary.

Routing (cursor.py):

# Standard path (99% of calls): no setinputsizes -> all in C++# Legacy path: setinputsizes active -> Python type detection + DDBCSQLExecuteLegacy

Performance Results 🚀

The Python-side type detection cost was ~2.0–2.3µs per parameter — an isinstance check, ParamInfo object construction, and a pybind11 FFI boundary crossing per parameter, per execute call. The C++ path replaces this with ~35ns/param (raw PyLong_Check + struct field write) — a ~60x faster per-parameter detection.

macOS arm64 (Apple Silicon M-series), Python 3.13

ScenarioAfter (C++)BeforeImprovementPython overhead eliminated
3 params435µs444µs2% faster10µs saved
10 params321µs344µs7% faster22µs saved
50 params394µs502µs21% faster107µs saved
100 params558µs770µs28% faster212µs saved
200 params644µs1.08ms40% faster432µs saved
500 params708µs1.82ms61% faster1.12ms saved
1024 params1.07ms3.32ms68% faster2.25ms saved

Linux aarch64 (Docker container), Python 3.13

ScenarioAfter (C++)BeforeImprovementPython overhead eliminated
3 params158µs164µs4% faster6µs saved
10 params159µs178µs11% faster19µs saved
50 params170µs271µs37% faster101µs saved
100 params209µs411µs49% faster202µs saved
200 params280µs651µs57% faster371µs saved
500 params396µs1.35ms71% faster956µs saved
1024 params746µs2.75ms73% faster2.0ms saved

vs pyodbc (post-PR, macOS)

Paramsmssql-pythonpyodbcGap
3399µs388µs3% gap (near parity)
50458µs452µs1% gap (parity)
200604µs550µs10% gap (down from ~14x pre-PR)
10241.31ms979µs33% gap (binding overhead, addressable separately)

Customer scenarios (end to end, macOS arm64)

The numbers above isolate driver overhead. These are whole insert workloads, so they also carry the network round trip and SQL Server actually writing the rows, which this PR does not change and which dilutes the percentage. Measured against the merge base (d94debd) with the two builds interleaved across 3 rounds, 7 iterations each, first 2 discarded.

ScenarioBeforeAfterSpeedup
Orders insert (int, varchar, decimal, datetime2)880.6ms561.0ms1.57x
Event log insert (uuid, datetime2, varchar, int)827.2ms533.5ms1.55x
Document insert (nvarchar(max) ~10KB, DAE)1622.0ms1021.3ms1.59x
Wide row insert (50 mixed columns)1598.6ms1048.9ms1.52x
Single-row execute x5000 (4 params)2103.5ms2095.9ms1.00x

Between them these cover every parameter type whose detection moved: int, varchar, decimal, datetime2, uuid, and the nvarchar(max) DAE streaming path.

Single-row execute does not move, and that is the expected result rather than a disappointment. At roughly 420µs per call the cost is the network round trip; detection for 4 parameters was only ever about 9µs of it. The gain scales with parameters per execute, so batched and wide-row work benefits and one-row-at-a-time work stays where it was.

Bottom line

MetricValue
Avg improvement (50+ params)~50% faster execute()
Worst-case improvement (1024 params)73% faster (2ms saved per call)
Per-param overhead reduction~60x (2.3µs → 35ns)
pyodbc gap closedFrom 14x slower (GH-500) to <10% gap at 200 params
Real-world impactBulk inserts see 1.5–1.6x throughput end to end (customer scenarios above)

Checklist

  • Tested locally (macOS arm64 + Linux aarch64)
  • Verified perf gain with micro-benchmarks on both platforms, and with end-to-end customer scenarios against the merge base
  • CI passing (CodeQL, DevSkim, Black, C++ lint)
  • No breaking changes to public API
  • setinputsizes users unaffected (routed to legacy path)

Move parameter type detection from Python into C++ using raw CPython
type checks (PyLong_CheckExact, PyFloat_CheckExact, etc.). Merge the
DetectParamTypes → BindParameters → SQLExecute pipeline into a single
DDBCSQLExecuteFast call so ParamInfo never crosses the pybind11 boundary.
- DetectParamTypes: handles int (range-detected), float, bool, str
(unicode + geometry sniffing), bytes, datetime/date/time, Decimal
(MONEY range + generic numeric), UUID, None, with fallback to string
- SQLExecuteFast_wrap: single pipeline with GIL release, always uses
SQLPrepare for parameterized queries
- cursor.py: fast path routing when no setinputsizes overrides present;
old DDBCSQLExecute path preserved for setinputsizes callers
- Named constants: MAX_INLINE_CHAR, MAX_INLINE_BINARY, MAX_NUMERIC_PRECISION,
MONEY/SMALLMONEY ranges, PARAM_C_TYPE_TEXT platform macro
Comment threadmssql_python/pybind/ddbc_bindings.cpp Fixed
- Add complete DAE (Data-At-Execution) loop to SQLExecuteFast_wrap:
SQL_NEED_DATA → SQLParamData/SQLPutData for large str/bytes/binary,
matching the existing SQLExecute_wrap logic exactly
- Fix DAE type assignment: non-unicode DAE strings use SQL_C_CHAR
(not PARAM_C_TYPE_TEXT which maps to SQL_C_WCHAR on macOS/Linux)
- Fix MONEY range lower bound: use MONEY_MIN not SMALLMONEY_MIN so
negative decimals in MONEY range bind as VARCHAR (matches Python path)
- Raise TypeError for unknown param types instead of silent str conversion
- Add SQLFreeStmt(SQL_RESET_PARAMS) to unbind after execute
@github-actions

github-actionsBot commented Apr 29, 2026

Copy link
Copy Markdown

📊 Code Coverage Report

🔥 Diff Coverage

89%


🎯 Overall Coverage

82%


📈 Total Lines Covered:7751 out of 9430
📁 Project:mssql-python


Diff Coverage

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

  • mssql_python/cursor.py (100%)
  • mssql_python/pybind/ddbc_bindings.cpp (81.8%): Missing lines 247,576-577,639,707,1810-1811,1954-1955,2050-2051,2054-2057,2069-2077,2102-2103,2133-2135,2156,2173,2189-2192,2202,2621,2736,4422
  • mssql_python/pybind/ddbc_bindings.h (100%)
  • mssql_python/pybind/param_detect.hpp (91.6%): Missing lines 275-276,282-283,424-425,431-432,456-457,469-474,507-512,546-547,563-564,580-581,612-613,658-659
  • mssql_python/pybind/py_ref.hpp (100%)
  • mssql_python/pybind/py_type_cache.hpp (92.7%): Missing lines 48-51

Summary

  • Total: 693 lines
  • Missing: 75 lines
  • Coverage: 89%

mssql_python/pybind/ddbc_bindings.cpp

Lines 243-251

243 SQLDescribeParamFunc SQLDescribeParam_ptr = nullptr;
244245namespace {
246 ! 247248constchar* GetSqlCTypeAsString(constSQLSMALLINT cType) {
249switch (cType) {
250STRINGIFY_FOR_CASE(SQL_C_CHAR);
251STRINGIFY_FOR_CASE(SQL_C_WCHAR);

Lines 572-581

572 dataPtr = sqlwcharBuffer->data();
573 bufferLength = sqlwcharBuffer->size() * sizeof(SQLWCHAR);
574 strLenOrIndPtr = AllocateParamBuffer<SQLLEN>(paramBuffers);
575// Use explicit byte length instead of SQL_NTS so embedded NUL chars
! 576// aren't treated as string terminators.
! 577 *strLenOrIndPtr = static_cast<SQLLEN>(sqlwcharBuffer->size() * sizeof(SQLWCHAR));
578 }
579break;
580 }
581caseSQL_C_BIT: {

Lines 635-643

635caseSQL_C_LONG: {
636if (!py::isinstance<py::int_>(param)) {
637ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex));
638 }
! 639// Both detection paths (DetectParamTypes / _map_sql_type) reject out-of-int64640// ints before binding, so those callers only reach here with bindable values.641// A setinputsizes() override that forces SQL_C_SBIGINT on an out-of-range int642// skips detection; that value fails the cast below, same as before this change.643 dataPtr = static_cast<void*>(

Lines 703-711

703 dataPtr = static_cast<void*>(sqlTimePtr);
704break;
705 }
706caseSQL_C_SS_TIMESTAMPOFFSET: {
! 707 py::object datetimeType = PyTypeCache::get_datetime_class_obj();
708if (!py::isinstance(param, datetimeType)) {
709ThrowStdException(MakeParamMismatchErrorStr(paramInfo.paramCType, paramIndex));
710 }
711// Checking if the object has a timezone

Lines 1806-1815

18061807// LEGACY — slated for removal in a future optimization round.1808//1809// Executes the provided query using a ParamInfo list that Python already built,
! 1810// rather than detecting parameter types in C++. Retained only for setinputsizes()
! 1811// callers, whose explicit type overrides the native path does not yet honour.1812// Every parameter crosses the pybind11 boundary as a ParamInfo object here, which1813// is the cost SQLExecute_wrap exists to avoid. Once setinputsizes is handled1814// natively this function and its DDBCSQLExecuteLegacy binding both go away.1815//

Lines 1950-1959

1950if (matchedInfo->paramCType == SQL_C_WCHAR) {
1951 std::u16string utf16 =
1952 borrow<py::str>(pyObj).cast<std::u16string>();
1953 rc = stream_dae_chunks(
! 1954reinterpretU16stringAsSqlWChar(utf16),
! 1955 utf16.size() * sizeof(SQLWCHAR),
1956 putData);
1957if (!SQL_SUCCEEDED(rc)) {
1958LOG("SQLExecute: SQLPutData failed for SQL_C_WCHAR DAE streaming");
1959return rc;

Lines 2046-2061

2046returnSQL_INVALID_HANDLE;
2047 }
20482049SQLHANDLE hStmt = statementHandle->get();
! 2050 ! 2051// Configure forward-only / read-only cursor (matches slow path semantics).2052if (SQLSetStmtAttr_ptr) {
2053SQLSetStmtAttr_ptr(hStmt, SQL_ATTR_CURSOR_TYPE,
! 2054 (SQLPOINTER)SQL_CURSOR_FORWARD_ONLY, 0);
! 2055SQLSetStmtAttr_ptr(hStmt, SQL_ATTR_CONCURRENCY,
! 2056 (SQLPOINTER)SQL_CONCUR_READ_ONLY, 0);
! 2057 }
20582059// The encoding-settings dict has the form {"encoding": str, "ctype": int}.2060// Note: the Python layer's SQL_C_CHAR constant is numerically -8, the same2061// as ODBC's SQL_C_WCHAR. As a result, the only path that genuinely uses

Lines 2065-2081

2065// encoding when ctype == 1 (real ODBC SQL_CHAR). Otherwise the user's2066// "encoding" value is meant for the wide-char path and we leave it alone.2067 std::string charEncoding = "utf-8";
2068if (encoding_settings.contains("ctype") && encoding_settings.contains("encoding")) {
! 2069int ctype = encoding_settings["ctype"].cast<int>();
! 2070if (ctype == SQL_C_CHAR/* real ODBC value: 1 */) {
! 2071 charEncoding = encoding_settings["encoding"].cast<std::string>();
! 2072 }
! 2073 }
! 2074 ! 2075// The cursor.py caller always passes a fresh `list(actual_params)` so this
! 2076// function is free to mutate slots in place. Even so, every site below uses
! 2077// PyList_SetItem (which decrefs the old slot before stealing the new ref),2078// so the function is safe regardless of who owns the list.20792080// Run DetectParamTypes BEFORE SQLPrepare so that type-detection errors2081// (unsupported type, NaN Decimal, precision overflow) don't leave the

Lines 2098-2107

2098 }
2099if (!SQL_SUCCEEDED(rc)) return rc;
2100 statementHandle->clearDescribeCache();
2101 is_stmt_prepared[0] = py::bool_(true);
! 2102 } else {
! 2103ThrowStdException("Cannot execute unprepared statement");
2104 }
2105 }
21062107 std::vector<std::shared_ptr<void>> paramBuffers;

Lines 2129-2139

2129 rc = SQLParamData_ptr(hStmt, &paramToken);
2130 }
2131if (rc != SQL_NEED_DATA) break;
2132 ! 2133// The DAE token is the &paramInfos[i] we handed to SQLBindParameter as the
! 2134// parameter value (see BindParameters), and paramInfos is sized up front and
! 2135// never reallocated, so the token casts straight back to its ParamInfo instead2136// of scanning. Range-check it against the vector before trusting it, so a bogus2137// token throws rather than dereferencing arbitrary memory.2138const ParamInfo* matchedInfo = reinterpret_cast<const ParamInfo*>(paramToken);
2139const ParamInfo* first = paramInfos.data();

Lines 2152-2160

2152if (matchedInfo->paramCType == SQL_C_WCHAR) {
2153 std::u16string u16 =
2154 borrow<py::str>(pyObj).cast<std::u16string>();
2155 rc = stream_dae_chunks(
! 2156reinterpretU16stringAsSqlWChar(u16),
2157u16.size() * sizeof(SQLWCHAR),
2158 putData);
2159if (!SQL_SUCCEEDED(rc)) return rc;
2160 } elseif (matchedInfo->paramCType == SQL_C_CHAR) {

Lines 2169-2177

2169 }
2170 } elseif (PyBytes_Check(pyObj) || PyByteArray_Check(pyObj)) {
2171// matchedInfo->dataPtr holds a strong ref to pyObj for the whole loop.2172constchar* dataPtr = nullptr;
! 2173size_t totalBytes = 0;
2174 std::string bytesStorage; // only used for the bytearray copy below21752176if (PyBytes_Check(pyObj)) {
2177// bytes is immutable and kept alive by the strong ref above, so stream

Lines 2185-2196

2185 bytesStorage.assign(PyByteArray_AS_STRING(pyObj),
2186static_cast<size_t>(PyByteArray_GET_SIZE(pyObj)));
2187 dataPtr = bytesStorage.data();
2188 totalBytes = bytesStorage.size();
! 2189 }
! 2190 ! 2191 rc = stream_dae_chunks(dataPtr, totalBytes, putData);
! 2192if (!SQL_SUCCEEDED(rc)) return rc;
2193 } else {
2194ThrowStdException("SQLExecute: DAE only supported for str or bytes");
2195 }
2196 }

Lines 2198-2206

2198 }
21992200if (!SQL_SUCCEEDED(rc) && rc != SQL_NO_DATA) return rc;
2201 ! 2202// Unbind parameter buffers before they go out of scope.2203// Not called on error paths — diagnostics must remain readable.2204SQLRETURN exec_rc = rc;
2205SQLFreeStmt_ptr(hStmt, SQL_RESET_PARAMS);
2206return exec_rc;

Lines 2617-2625

2617 DateTimeOffset* dtoArray =
2618 AllocateParamBufferArray<DateTimeOffset>(tempBuffers, paramSetSize);
2619 strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
2620 ! 2621 py::object datetimeType = PyTypeCache::get_datetime_class_obj();
26222623for (size_t i = 0; i < paramSetSize; ++i) {
2624const py::handle& param = columnValues[i];

Lines 2732-2740

27322733// Get cached UUID class from module-level helper2734// This avoids static object destruction issues during2735// Python finalization
! 2736 py::object uuid_class = PyTypeCache::get_uuid_class_obj();
2737// Get cached UUID class27382739for (size_t i = 0; i < paramSetSize; ++i) {
2740const py::handle& element = columnValues[i];

Lines 4418-4426

4418break;
4419 }
4420caseSQL_TYPE_DATE: {
4421 PyObject* dateObj =
! 4422PyTypeCache::get_date_class_obj()(buffers.dateBuffers[col - 1][i].year,
4423 buffers.dateBuffers[col - 1][i].month,
4424 buffers.dateBuffers[col - 1][i].day)
4425 .release()
4426 .ptr();

mssql_python/pybind/param_detect.hpp

Lines 271-280

271if (!as_str) {
272// PyObject_Str can fail (e.g. CPython's int->str digit limit for a273// multi-thousand-digit int). Drop that error and fall back to a274// placeholder so we still raise our own clear ValueError.
! 275PyErr_Clear();
! 276 }
277 std::string s = as_str ? as_str.cast<std::string>() : std::string("<int>");
278throwpy::value_error("integer " + s +
279" is out of range for SQL BIGINT [-2^63, 2^63-1]");
280 } else {

Lines 278-287

278throwpy::value_error("integer " + s +
279" is out of range for SQL BIGINT [-2^63, 2^63-1]");
280 } else {
281// A real Python error from PyLong_AsLongLongAndOverflow, not overflow.
! 282throwpy::error_already_set();
! 283 }
284 info.decimalDigits = 0;
285continue;
286 }

Lines 420-429

420// so calling the same method is what keeps the two paths in agreement.421 py::object time_obj = steal(PyObject_CallMethod(obj, "isoformat", "s", "microseconds"));
422if (!time_obj) throwpy::error_already_set();
423if (!PyUnicode_Check(time_obj.ptr())) {
! 424throwpy::type_error("datetime.time.isoformat() must return a str");
! 425 }
426 Py_ssize_t time_len = PyUnicode_GET_LENGTH(time_obj.ptr());
427 info.columnSize = std::max<SQLULEN>(info.columnSize, time_len);
428// PyList_SetItem (lowercase) decrefs the old slot before stealing the new429// reference; safe here because cursor.py already passed a fresh list copy.

Lines 427-436

427 info.columnSize = std::max<SQLULEN>(info.columnSize, time_len);
428// PyList_SetItem (lowercase) decrefs the old slot before stealing the new429// reference; safe here because cursor.py already passed a fresh list copy.430if (PyList_SetItem(params, i, time_obj.release().ptr()) != 0) {
! 431throwpy::error_already_set();
! 432 }
433continue;
434 }
435436// --- Decimal ---

Lines 452-461

452 py::object digits_obj = steal(PyObject_GetAttrString(as_tuple_ptr.ptr(), "digits"));
453if (!digits_obj) throwpy::error_already_set();
454455if (!PyTuple_Check(digits_obj.ptr())) {
! 456throwpy::type_error("Decimal.as_tuple().digits must be a tuple");
! 457 }
458459 Py_ssize_t num_digits = PyTuple_GET_SIZE(digits_obj.ptr());
460461// Read the exponent at full width and range-check it BEFORE narrowing to int.

Lines 465-478

465// any precision, so treat overflow as precision overflow rather than propagating466// OverflowError, matching what the legacy Python path reports.467longlong exponent_ll = PyLong_AsLongLong(exponent_obj.ptr());
468if (exponent_ll == -1 && PyErr_Occurred()) {
! 469PyErr_Clear();
! 470throwpy::value_error(
! 471"Precision of the numeric value is too high. "
! 472"The maximum precision supported by SQL Server is " +
! 473std::to_string(MAX_NUMERIC_PRECISION) + ".");
! 474 }
475// Bound before any arithmetic or negation. MAX_NUMERIC_PRECISION on both sides is476// wider than anything bindable, and keeps -exponent well clear of INT_MIN, whose477// negation would be signed-overflow UB.478if (exponent_ll > MAX_NUMERIC_PRECISION || exponent_ll < -MAX_NUMERIC_PRECISION) {

Lines 503-516

503else504 precision = -exponent;
505506if (precision > MAX_NUMERIC_PRECISION) {
! 507throwpy::value_error(
! 508"Precision of the numeric value is too high. "
! 509"The maximum precision supported by SQL Server is " +
! 510std::to_string(MAX_NUMERIC_PRECISION) + ", but got " +
! 511std::to_string(precision) + ".");
! 512 }
513514// Check SMALLMONEY first, then widen to MONEY, so common small values keep the narrowest515// exact range while still accepting larger fixed-point values supported by SQL Server.516// MONEY/SMALLMONEY: SQL Server stores these as fixed-point integers internally.

Lines 542-551

542 PyObject* raw = formatted.release().ptr();
543if (PyList_SetItem(params, i, raw) != 0) {
544// PyList_SetItem steals (decrefs) the item even on failure,545// so raw is already freed — do NOT Py_DECREF here.
! 546throwpy::error_already_set();
! 547 }
548continue;
549 }
550551// Build SQL_NUMERIC_STRUCT from the Decimal object. Store as a pybind11-castable

Lines 559-568

559 py::object numeric_obj = py::cast(nd);
560 PyObject* raw = numeric_obj.release().ptr();
561if (PyList_SetItem(params, i, raw) != 0) {
562// PyList_SetItem steals (decrefs) the item even on failure.
! 563throwpy::error_already_set();
! 564 }
565continue;
566 }
567568// --- UUID ---

Lines 576-585

576 info.columnSize = 16;
577 info.decimalDigits = 0;
578if (PyList_SetItem(params, i, bytes_le) != 0) {
579// PyList_SetItem steals (decrefs) the item even on failure.
! 580throwpy::error_already_set();
! 581 }
582continue;
583 }
584585// --- Unknown type: raise TypeError (matches Python _map_sql_type) ---

Lines 608-617

608int sign_val = static_cast<int>(PyLong_AsLong(sign_obj.ptr()));
609if (sign_val == -1 && PyErr_Occurred()) throwpy::error_already_set();
610611if (!PyTuple_Check(digits)) {
! 612throwpy::type_error("Decimal.as_tuple().digits must be a tuple");
! 613 }
614615// SQL Server precision counts all stored decimal digits, while scale is just the616// fractional digits. A positive exponent moves trailing zeros into the integer part;617// a negative exponent means scale = -exponent and precision must still cover leading

Lines 654-663

654for (int j = 0; j < exponent; ++j) {
655 overflow |= mul10_add(0);
656 }
657if (overflow != 0) {
! 658throwpy::value_error("Decimal magnitude exceeds the 16-byte SQL NUMERIC capacity");
! 659 }
660661 NumericData nd;
662 nd.precision = static_cast<SQLCHAR>(precision);
663 nd.scale = static_cast<SQLSCHAR>(scale);

mssql_python/pybind/py_type_cache.hpp

Lines 44-55

44// type detection in Python and can therefore reach here without the cache being warm;45// it can be dropped once that path is removed.46inline PyObject* get_cached_class(PyObject* cached, constchar* module_name, constchar* attr_name) {
47if (cache_initialized && cached) return cached;
! 48 py::object mod = steal(PyImport_ImportModule(module_name));
! 49if (!mod) returnnullptr;
! 50returnPyObject_GetAttrString(mod.ptr(), attr_name);
! 51 }
5253// One-time init. Uses local py::objects so exception cleanup is automatic;54// only .release() into globals after ALL acquisitions succeed.55inlinevoidinitialize() {


📋 Files Needing Attention

📉 Files with overall lowest coverage (click to expand)
mssql_python.pybind.logger_bridge.cpp: 59.2%
mssql_python.pybind.ddbc_bindings.h: 61.5%
mssql_python.pybind.logger_bridge.hpp: 70.8%
mssql_python.pybind.ddbc_bindings.cpp: 75.5%
mssql_python.__init__.py: 77.6%
mssql_python.row.py: 77.6%
mssql_python.ddbc_bindings.py: 79.6%
mssql_python.pybind.connection.connection_pool.cpp: 81.4%
mssql_python.pybind.connection.connection.cpp: 84.3%
mssql_python.logging.py: 85.5%

🔗 Quick Links

⚙️ Build Summary📋 Coverage Details

View Azure DevOps Build

Browse Full Coverage Report

- Comment out use_prepare parameter name (C4100: unreferenced parameter)
- Remove unused catch variable name (C4101: unreferenced local variable)
Add explicit null pointer and zero-length guards before memcpy in
build_numeric_data to satisfy DevSkim code scanning rule DS121708.
Comment threadmssql_python/pybind/ddbc_bindings.cpp Fixed
@github-actionsgithub-actionsBot added the pr-size: large Substantial code update label May 7, 2026
…or attrs, parity test
Six review fixes for SQLExecuteFast_wrap and DetectParamTypes:
1. Encoding key: read 'encoding' from settings dict (was 'charEncoding'
which never matched). Only honor when ctype==SQL_C_CHAR so the default
utf-16le doesn't corrupt SQL_C_CHAR DAE/inline byte paths.
2. Subclass support: PyLong_Check/PyFloat_Check/PyUnicode_Check/PyBytes_Check
instead of *_CheckExact. Fixes user-defined int/str/bytes/float
subclasses that were silently rejected with TypeError. Switched
PyBytes_GET_SIZE to PyBytes_Size for subclass-safe length.
3. GIL release in DAE loop: SQLParamData and SQLPutData now release the
GIL during each ODBC call, matching slow-path concurrency for large
blobs/strings.
4. Preserve exec_rc: stash the SQLExecute return code before SQLFreeStmt
so SUCCESS_WITH_INFO and other non-success-non-error codes are not
clobbered by the unbind call.
5. Shallow-copy params: params = py::list(params) at function entry so
DetectParamTypes' in-place PyList_SET_ITEM cannot mutate the caller's
list under any future code path that might pass it directly.
6. Cursor attrs: SQLSetStmtAttr(SQL_ATTR_CURSOR_TYPE/CONCURRENCY) at
entry to match slow-path semantics regardless of prior hstmt state.
Also adds tests/test_023_fast_path_parity.py covering int/str/bytes/float
subclasses, caller-list non-mutation, and unsupported-type TypeError.
Comment threadtests/test_023_fast_path_parity.py Fixed
Eight follow-up fixes after review feedback on c5a827f.
1. Refcount leak (BLOCKER): replace PyList_SET_ITEM (uppercase, no decref of
old slot) with PyList_SetItem (decrefs old slot before stealing the new
reference) in DetectParamTypes time/Decimal/UUID branches. The previous
shallow-copy defense via py::list(params) was a no-op because pybind11s
list constructor only inc_refs an already-list argument.
2. Geometry + DAE conflict: gate the geometry-prefix override on the not-DAE
branch so a long POLYGON/POINT/LINESTRING string does not end up with
isDAE=true, dataPtr set, AND a non-zero columnSize.
3. Decimal NaN/Infinity: throw ValueError instead of silently binding 0 via
build_numeric_data on an empty digits tuple.
4. Time format: always emit microseconds (HH:MM:SS.ffffff), matching slow
path isoformat(timespec=microseconds).
5. PyObject_IsInstance: explicit equality check so a custom __instancecheck__
that raises (returns -1) does not fall through with a Python error set.
6. Dead code: removed unused SMALLMONEY_MIN/SMALLMONEY_MAX constants and the
unused utf16Len assignments in DetectParamTypes.
7. Encoding-key contract: only honor encoding_settings encoding when the
user explicitly opted in via setencoding(..., ctype=SQL_C_CHAR=1). The
Python layer SQL_C_CHAR constant is numerically -8 (real ODBC SQL_C_WCHAR),
so by default the wide-char path is taken and encoding is irrelevant.
8. Parity test rewrite: drop the dead _force_slow_path_roundtrip helper, use
the project cursor fixture instead of a hard-coded conn string, and add
(a) a real fast-vs-slow parity check via setinputsizes-forced slow path,
(b) a refcount-leak regression test using a Decimal subclass + weakref,
(c) explicit NaN-rejection coverage.
Resolve conflicts in ddbc_bindings.cpp from main's GH-610 work:
- Keep both build_numeric_data (this PR) and ResolveNullParamType (main)
- Adopt main's BindParameters/BindParameterArray signatures that take
SqlHandle& handle; update the SQLExecuteFast_wrap call site to pass
*statementHandle so the fast path uses the per-handle NULL describe cache
- Migrate SQLExecuteFast_wrap from std::wstring + WStringToSQLWCHAR to
std::u16string + reinterpretU16stringAsSqlWChar (main's uniform 16-bit
query/param representation), dropping the platform #ifdef in both the
prepare path and the DAE wide-char put-data loop
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment threadmssql_python/pybind/ddbc_bindings.cpp Fixed
- Honor use_prepare flag (was silently ignored, always preparing)
- Move DetectParamTypes before SQLPrepare to prevent half-prepared state
- Fix bytearray DAE crash (pybind11 bytes caster doesn't handle bytearray)
- Replace lossy double MONEY comparison with exact Decimal arithmetic
- Add SMALLMONEY range detection (was missing from fast path)
- Handle PyObject_IsInstance error return (-1) with proper exception propagation
- Clear describe cache on prepare (matching slow path)
- Add edge case tests: large bytearray/bytes/string DAE, MONEY boundaries,
Infinity rejection, embedded nulls
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Replace pybind11 .attr()/.cast<>() with raw CPython calls throughout
DetectParamTypes and build_numeric_data
- datetime/date/time: use PyDateTime_Check/PyDate_Check/PyTime_Check macros
and PyDateTime_TIME_GET_* accessors (requires PyDateTime_IMPORT)
- Decimal: PyObject_CallMethod/GetAttrString/RichCompareBool instead of
py::module_::import + py::object .attr() chains
- UUID: PyObject_GetAttrString("bytes_le") instead of py::handle .attr()
- Cache MONEY/SMALLMONEY Decimal bounds in PythonObjectCache (constructed
once at init, not per-call) using cached Python-side constants
- Replace magic int range numbers with UINT8_MAX/INT16_MIN/MAX/INT32_MIN/MAX
- Proper Py_DECREF cleanup on all error paths in build_numeric_data
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment threadmssql_python/pybind/ddbc_bindings.cpp Fixed
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment threadmssql_python/pybind/ddbc_bindings.cpp Fixed
…ecuteLegacy
The new C++ pipeline is the primary path (99% of calls). The old function
is the legacy fallback for setinputsizes users only. Naming should reflect this.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@bewithgauravGaurav Sharma (bewithgaurav) changed the title PERF: Add C++ DetectParamTypes + SQLExecuteFast pipelinePERF: Native C++ parameter detection and execute pipelineJul 14, 2026
Removes unnecessary pybind11 ↔ CPython round-trips in the hot path:
- PythonObjectCache types stored as PyObject* (not py::object)
- ParamInfo::dataPtr is raw PyObject* with explicit refcount management
- DetectParamTypes takes PyObject* directly (not py::list&)
- build_numeric_data returns NumericData struct (not py::object)
- Added contextual comments explaining non-obvious design decisions
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ility
pybind11's type_caster needs copy semantics for std::vector<ParamInfo>&
in the legacy path. Provide a copy ctor that Py_XINCREFs dataPtr.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Strings with embedded NUL characters (e.g., 'hello\x00world') were
truncated at the first NUL because BindParameters used SQL_NTS
(null-terminated string indicator). Now passes the actual byte/char
length so ODBC sees the full string.
Fixes test_string_with_embedded_nulls on all platforms.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add tests: integer overflow (2**63), Decimal NaN/sNaN, precision > 38
- Add LCOV_EXCL markers on CPython import-failure and cache-fallback paths
- Add contextual comments on PythonObjectCache and ParamInfo operators
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment threadtests/test_023_execute_path_parity.py Fixed
CopilotAI added 2 commits August 1, 2026 16:16
py_ref.hpp existed earlier in this branch as the home for the custom PyPtr wrapper, and was deleted when PyPtr was replaced by py::object. steal() had to land somewhere, so it went into py_type_cache.hpp, which was already using it. borrow() then followed it there. neither belongs in that file: its own first line describes it as a cache of Python type objects and MONEY boundary constants, and these two helpers are neither.
restores py_ref.hpp with the reference-adoption helpers and nothing else, and gives py_type_cache.hpp back a description that matches its contents. only ddbc_bindings.cpp includes either header, so the include change is one line.
no behavior change. rebuilt and the .so is byte-identical to the previous commit (sha256 43ec909d...), with ddbc_bindings.cpp recompiled and relinked rather than served from cache.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
this PR added roughly 430 lines of new parameter-detection code into ddbc_bindings.cpp, a file that was already 6600 lines. DetectParamTypes, build_numeric_data and the types they produce are the first stage of the execute pipeline and are cohesive enough to read on their own, so they now live in their own header. ddbc_bindings.cpp drops to 6064 lines and only the pieces this PR introduced moved, so no pre-existing code shifts and no other in-flight branch gains a conflict.
the SQL Server ODBC constants that sql.h does not expose move from ddbc_bindings.cpp up into ddbc_bindings.h, because both the detection path and the fetch paths need them and the header is included before either.
header rather than .cpp on purpose. the build is -O3 with no LTO, so a .cpp boundary is also an inlining boundary, and these helpers run once per parameter per execute. defining them inline in a header keeps them in the including translation unit. once LTO is enabled this can become a normal .cpp.
the resulting binary is not quite bit-identical and the reason is worth stating: __text grows 92 bytes and DetectParamTypes gains an out-of-line symbol. previously it sat in an anonymous namespace with exactly one call site, so the compiler inlined it into SQLExecute_wrap and deleted the original; as an inline function with vague linkage it is now emitted once and called. that is one call per execute(), not per parameter, against a roughly 300us execute. build_numeric_data, which does run per decimal parameter, was already out-of-line before this change and still is: the only difference in its symbol is the mangled name losing the anonymous-namespace prefix. no other symbol changed.
1922 tests pass, the refcount harness reports zero drift across all 15 parameter cases over 300 executes, and the 7 DAE round-trip cases remain byte-exact.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment threadmssql_python/pybind/param_detect.hpp Dismissed
…oval
the two execute entry points are DDBCSQLExecute and DDBCSQLExecuteLegacy, so the surrounding code should read standard versus legacy. cursor.py still called the non-legacy branch use_fast_path, which named a third thing that does not exist and left the reader guessing which C++ function it reached. renamed to use_standard_execute, and the parity test file follows: test_023_fast_path_parity.py becomes test_023_execute_path_parity.py, with _fast_path_roundtrip becoming _standard_roundtrip.
every legacy site now says out loud that it is temporary and why it still exists. the legacy branch survives only for setinputsizes() callers, whose explicit type overrides the native path does not yet honour; that is the single thing blocking its removal, and it was not written down anywhere. annotated in cursor.py at the branch and the call, on SQLExecuteLegacy_wrap, on the DDBCSQLExecuteLegacy binding, and on the PyTypeCache import fallback that exists only because the legacy path can run before the cache is warm.
_create_parameter_types_list gets a fuller docstring rather than a removal note, because it has two callers and only one of them is legacy: executemany() still needs it and will keep needing it until columnwise detection is native too. calling it simply legacy would have been wrong.
left alone: the 'Fast path: Data fits in buffer' comments in ddbc_bindings.h and the ASCII-prefix fast path in test_002 and test_014. same words, unrelated concept, pre-existing.
comments and identifiers only, no logic touched. 1922 tests pass and the renamed parity file runs all 51 of its tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment threadtests/test_023_execute_path_parity.py Fixed
Comment threadmssql_python/pybind/param_detect.hpp Outdated
Comment threadmssql_python/pybind/param_detect.hpp Outdated
Comment threadmssql_python/pybind/param_detect.hpp Outdated
Comment threadmssql_python/pybind/param_detect.hpp Outdated
Comment threadtests/test_023_execute_path_parity.py
Comment threadmssql_python/pybind/param_detect.hpp
Comment threadmssql_python/pybind/ddbc_bindings.cpp
Comment threadmssql_python/pybind/ddbc_bindings.cpp Outdated
Comment threadmssql_python/pybind/ddbc_bindings.cpp Outdated
Comment threadmssql_python/pybind/ddbc_bindings.cpp Outdated
CopilotAIand others added 4 commits August 6, 2026 15:40
…t for time
two parity divergences from jahnvi480's review, both of which bound wrong data silently rather than failing.
the Decimal exponent was cast to int before it was validated. Decimal exponents are arbitrary precision, so on LP64 Decimal('1E+4294967297') truncated to 1, passed the precision <= 38 gate, and bound 10. Decimal('1E+2147483648') truncated to exactly INT_MIN and bound 0.1, and negating INT_MIN a few lines later is signed-overflow UB. the legacy Python path computes precision in arbitrary-precision ints and raises for both. the exponent is now read with PyLong_AsLongLong and bounded, along with the digit count, before any narrowing or arithmetic; an overflow from that read is reported as precision overflow rather than leaking OverflowError.
the time path hand-formatted HH:MM:SS.ffffff from the raw fields, which dropped tzinfo and ignored isoformat overrides on subclasses. an aware time whose isoformat is 01:02:03.000004+05:30 bound as 01:02:03.000004, a different time than the caller passed. it now calls isoformat(timespec='microseconds'), which is what _normalize_time_param does on the legacy side. SQL Server TIME has no offset so both paths now raise DataError for an aware time, verified against legacy auto-detection through executemany.
also finishes the fast_path rename from 2ecda91, which left four SQLExecuteFast strings in error messages and a stale comment in ddbc_bindings.cpp, plus six references in the parity test file.
12 tests added covering 2**32+1, INT_MIN, INT_MAX and their negatives, the 37 and -38 exponents that must still bind, and the aware/naive time pair. verified as real guards: reverting the header and rebuilding fails exactly the 2**32+1, INT_MIN and aware-time cases. 1934 tests pass, refcount harness reports zero drift.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The native detector resolved its text C type to SQL_C_WCHAR on Linux/macOS
but to a real SQL_C_CHAR (1) on Windows. The legacy Python path binds text
with the Python layer's SQL_C_CHAR constant, which is numerically -8, i.e.
ODBC's SQL_C_WCHAR, so the legacy path has always bound text wide on every
platform. Windows was therefore the only place where the two paths disagreed
on C type and on the driver-side encoding path they took, and it was also the
one combination CI never compared against a passing wide-bound baseline.
Bind wide everywhere. Three call sites share the constant: ASCII strings
(inline and DAE), datetime.time normalized to text, and MONEY-range Decimals
formatted to text, so all three change on Windows only.
Adds round-trip tests over ASCII, non-ASCII, inline/DAE boundary strings,
NVARCHAR conversion, time and MONEY, so a reintroduced narrow binding shows
up as a Windows-only failure.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The native detector raises ValueError for NaN, sNaN and Infinity. The legacy
Python path instead set precision=38 and carried on, so the failure happened
incidentally and with a different type each time: NaN raised
decimal.InvalidOperation from the MONEY range comparison in _map_sql_type,
while Infinity reached _get_numeric_data and raised TypeError from comparing
a str exponent against an int. Callers writing `except ValueError` saw
different behaviour depending on whether setinputsizes happened to be set.
Raise ValueError with the same message in both _map_sql_type and
_get_numeric_data. _get_numeric_data needs its own check because executemany's
typing pass reaches it directly.
Tightens the existing rejection tests from `raises(Exception)` to the exact
type, and adds a parity test asserting both paths raise ValueError for all
five non-finite forms.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment threadtests/test_023_execute_path_parity.py Dismissed
The old parity suite claimed to compare native (C++ DetectParamTypes) against
legacy (Python _map_sql_type) detection, but its "legacy" helper reached the
legacy path via setinputsizes — which supplies explicit types and bypasses
_map_sql_type entirely. So it compared native detection against types hardcoded
in the test, never ran the Python detector, and asserted on the round-tripped
value, which SQL Server coercion can mask. Every real divergence found on this
PR (geometry >4000, aware time, Windows narrow binding) was found by reading
code; the suite was green through all of them. Coverage confirmed it:
_map_sql_type's body (lines 431-719) and _get_numeric_data sat in the Missing
list.
Drop the forcing. Test each path through the door real callers use:
- Native path: end-to-end via cursor.execute(), unchanged.
- Python detection: assert _map_sql_type(value, [value], 0) directly as a pure
function returning the 5-tuple (SQL type, C type, column size, decimal digits,
DAE) — no DB round-trip, so coercion can't hide a wrong type. Covers every
branch: int widths, float, decimal money/numeric, uuid, ascii/unicode
inline/DAE strings, geometry, binary, date/datetime/time.
- _get_numeric_data: direct precision/scale and overflow assertions.
- Legacy execute path (DDBCSQLExecuteLegacy): kept, reached through its only
real entry point (setinputsizes), used for what it is for — user-supplied
type overrides — plus a shorter-than-params case that exercises the
_map_sql_type fallback in _create_parameter_types_list.
The long-POLYGON case pins the legacy contract and notes the native side still
diverges (a known open item, not fixed here) so the gap stays visible.
Net: this file's cursor.py coverage rises 30% -> 39%; 85 -> 128 tests.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…g kinds
Native geometry detection had two gaps the legacy path does not:
1. It was gated behind !info.isDAE, so a POINT/LINESTRING/POLYGON over 4000
UTF-16 units never got the geometry treatment — it fell through to the
generic long-string DAE path as SQL_VARCHAR instead of NVARCHAR.
2. It only inspected the 1-byte (ASCII) storage kind, so a WKT string carrying
any non-ASCII char (stored by CPython in a wider UCS-2/4 kind) was missed and
bound as VARCHAR. The legacy path uses str.startswith, which is
kind-independent.
Detect the geometry prefix kind-agnostically (new StartsWithAscii reads code
points via PyUnicode_READ) and fold the result into is_unicode BEFORE the
length/DAE branch, so geometry is always NVARCHAR in both size regimes.
Deliberately not a literal match to the legacy tuple: legacy _map_sql_type
returns NVARCHAR with columnSize == len and DAE=false even for a 7790-char
polygon, which is unbindable — SQLBindParameter rejects a non-MAX NVARCHAR
precision > 4000 with "Invalid precision value". Folding into is_unicode keeps
geometry wide while the existing length gate streams large values via DAE, which
actually binds and round-trips. A test pins that legacy defect so it stays
visible; native is verified via sql_variant BaseType (small + unicode-tagged)
and a >4000 round-trip through a real geometry column.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Two changes in the native SQLExecute DAE loop (the legacy DDBCSQLExecuteLegacy
loop is left alone since it is slated for removal).
Zero-copy bytes: the loop copied the whole payload into a std::string before
streaming, even for immutable bytes. matchedInfo->dataPtr already holds a strong
ref to the object for the duration, and bytes cannot be mutated, so the buffer is
stable across the GIL release. Stream straight from PyBytes_AS_STRING /
PyBytes_GET_SIZE and drop the copy. This is the large-blob path, so it avoids a
full payload copy per DAE bytes param. bytearray keeps its copy because it is
mutable across the GIL release.
Token cast-back: SQLParamData returns the &paramInfos[i] token we handed to
SQLBindParameter. paramInfos is sized up front and never reallocated, so the
token casts straight back to its ParamInfo instead of a linear scan of every
param per chunk. A range + alignment check keeps a bogus token throwing instead
of dereferencing arbitrary memory.
Verified: large bytes (incl. embedded NULs), bytearray, large unicode string, and
multiple DAE params in one execute all round-trip; full suite 2083 passed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Both are self-review cleanups on code added earlier in this PR, no behaviour
change. Full suite green (2184 passed).
StartsWithAscii now deduces the prefix length from the string literal via a
template non-type parameter instead of a hand-passed count, so the POINT /
LINESTRING / POLYGON call sites can no longer drift a length out of sync with
the literal.
The DAE token cast-back drops the element-alignment modulo check and keeps only
the range check. SQLParamData returns the exact &paramInfos[i] pointer we handed
SQLBindParameter, so a valid token is always element-aligned; the modulo could
only ever matter for an already-corrupt token, which the range check already
rejects. Removing it drops three lines of defense against an unreachable state.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sumitmsft

Copy link
Copy Markdown
Contributor

Nice work! I tried to trace the refcounting (steal/borrow RAII), GIL release around ODBC calls, DAE streaming, and legacy parity; it all holds up.

One optional, non-blocking follow-up:
"2**63" classifies as BIGINT and fails later at "param.cast<int64_t>()" with a generic message. This matches legacy "_map_sql_type" exactly, so it's faithful parity, not a regression. For a clearer "exceeds BIGINT range" error, raise at detect time on both paths. (Minor: the int64_t range guard in the bind case is dead code.) // Ref: param_detect.hpp

gargsaumya
gargsaumya previously approved these changes Aug 13, 2026

@gargsaumyagargsaumya 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.

LGTM. Earlier reviews covered the substantive items, approved.

…both paths
A Python int past signed int64 range (e.g. 2**63) was classified as SQL BIGINT
at detect time, then failed deep in binding at param.cast<int64_t>() with an
opaque pybind11 error ("Unable to cast Python instance of type <class 'int'> to
C++ type '?'"). SQL Server has no integer type wider than BIGINT, so these can
never bind.
Reject them at detection with a clear ValueError, identical message on both
paths:
- Native DetectParamTypes: the int-overflow branch now raises
"integer <n> is out of range for SQL BIGINT [-2^63, 2^63-1]" instead of
mislabelling as BIGINT. PyObject_Str failure (CPython's int->str digit limit
for very long ints) falls back to a placeholder and clears the error, so we
still raise our own ValueError. A genuine (non-overflow) Python error from
PyLong_AsLongLongAndOverflow now propagates instead of being swallowed.
- Legacy _map_sql_type: mirrors the same check and message before the
INT -> BIGINT return, using new BIGINT_MIN/BIGINT_MAX constants. Handles the
executemany column min/max case.
Removes two dead range guards in BindParameters (signed and unsigned): each
compared an already-cast fixed-width int against its own type limits, which is
always false and unreachable because the cast throws first. A setinputsizes()
override that forces SQL_C_SBIGINT on an out-of-range int still fails the cast,
unchanged by this commit.
Tightens test_integer_overflow_detected to assert the exact ValueError message
on both paths, and adds a boundary test that +/-2^63 still bind. Full suite
2185 passed.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@bewithgaurav
Gaurav Sharma (bewithgaurav) merged commit 4796d2c into mainAug 14, 2026
29 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-size: largeSubstantial code update

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@bewithgaurav@sumitmsft@jahnvi480@github-advanced-security@gargsaumya