Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions mssql_python/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1031,13 +1031,13 @@ def _reset_cursor(self) -> None:
self.is_stmt_prepared = [False]

def _soft_reset_cursor(self) -> None:
"""Lightweight reset: close cursor and unbind params without freeing the HSTMT.
"""Close results without freeing the HSTMT or compatible cached bindings.

Preserves the prepared statement plan on the server so repeated
executions of the same SQL skip SQLPrepare entirely.
"""
if self.hstmt:
ret = ddbc_bindings.DDBCSQLResetStmt(self.hstmt)
ret = ddbc_bindings.DDBCSQLResetStmt(self.hstmt, preserve_bindings=True)
try:
check_error(ddbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt, ret)
except Exception:
Expand Down
21 changes: 21 additions & 0 deletions mssql_python/pybind/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,27 @@

This README provides instructions to build the DDBC Bindings for your system and documents the platform-specific dependencies.

## Repeated execute bindings

Each statement owns at most one reusable generation of native input buffers.
The existing detector and binder still validate and convert every execution.
Bindings are reused only for the same prepared SQL, parameter count, C/SQL
types, column size, scale, direction, encoding, and actual buffer byte lengths.
Inline text/binary and integer, boolean, and floating-point buffers are supported,
up to 2,100 parameters and 8,000 bytes per text/binary buffer. NULL, DAE, and
complex C types use the uncached path; decimal overrides converted to text
can reuse only with matching precision and scale.

Soft cursor resets preserve successful cached bindings. New SQL, incompatible
metadata or byte lengths, explicit resets, direct/catalog/array execution,
statement-attribute changes, and errors invalidate reuse. Native storage remains
owned until ODBC resets the bindings or frees the statement, including error
paths and parent connection teardown. No Python references are retained in the
cache, and the existing DB-API `threadsafety=1` contract is unchanged.

`tests/test_037_cached_bindings.py` checks live round trips and actual native
allocation/bind events through the existing debug logger, without a test-only API.

## **Key Architecture Handling**

1. **Architecture Normalization** (from `mssql_python/ddbc_bindings.py`):
Expand Down
24 changes: 16 additions & 8 deletions mssql_python/pybind/connection/connection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,15 @@ void Connection::disconnect() {
LOG("Disconnecting from database");
}

// CRITICAL FIX: Mark all child statement handles as implicitly freed
// When we free the DBC handle below, the ODBC driver will automatically free
// all child STMT handles. We need to tell the SqlHandle objects about this
// so they don't try to free the handles again during their destruction.

// SQLDisconnect frees child statements. Retain their owners until it
// succeeds, then mark them so later cursor cleanup cannot double-free.

// THREAD-SAFETY: Lock mutex to safely access _childStatementHandles
// This protects against concurrent allocStatementHandle() calls or GC finalizers
size_t originalSize = 0, afterCompactSize = 0, badHandleCount = 0;
// Keep statement buffers alive through the parent's blocking disconnect.
// Do not mark children freed until SQLDisconnect actually succeeds.
std::vector<SqlHandlePtr> childHandles;
{
std::lock_guard<std::mutex> lock(_childHandlesMutex);

Expand All @@ -141,11 +142,9 @@ void Connection::disconnect() {
++badHandleCount;
continue; // Skip marking to prevent leak
}
handle->markImplicitlyFreed();
childHandles.push_back(std::move(handle));
}
}
_childStatementHandles.clear();
_allocationsSinceCompaction = 0;
} // Release lock before potentially slow SQLDisconnect call

// Log after releasing _childHandlesMutex (#671): LOG()/LOG_ERROR() acquire
Expand Down Expand Up @@ -181,6 +180,15 @@ void Connection::disconnect() {
// via py::gil_scoped_acquire, which is unsafe during interpreter
// shutdown or stack unwinding (can deadlock or call std::terminate).
}
if (SQL_SUCCEEDED(ret)) {
for (const auto& handle : childHandles) {
handle->markImplicitlyFreed();
handle->releaseAfterFree();
}
std::lock_guard<std::mutex> lock(_childHandlesMutex);
_childStatementHandles.clear();
_allocationsSinceCompaction = 0;
}
// triggers SQLFreeHandle via destructor, if last owner
_dbcHandle.reset();
Comment on lines +183 to 193
} else if (hasGil) {
Expand Down
Loading
Loading