Skip to content

Commit 394b63a

Browse files
authored
Merge branch 'main' into bewithgaurav/pyproject-modernization
2 parents 6a50152 + be55129 commit 394b63a

9 files changed

Lines changed: 467 additions & 44 deletions

File tree

PyPI_Description.md

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,27 +35,11 @@ PyBind11 provides:
3535
- Memory-safe bindings
3636
- Clean and Pythonic API, while performance-critical logic remains in robust, maintainable C++.
3737

38-
## What's new in v1.2.0
39-
40-
### Enhancements
41-
42-
- **Connection.closed Property** - Added `Connection.closed` property to check if a connection is closed, improving connection state management.
43-
44-
- **Parameter as Dictionary** - Added support for passing parameters as dictionaries, providing more flexible query parameterization.
45-
46-
- **Copilot Prompts** - Introduced Copilot prompts for AI-assisted development, enhancing developer productivity.
38+
## What's new in v1.3.0
4739

4840
### Bug Fixes
4941

50-
- **FetchMany with LOB Columns** - Fixed `fetchmany(n)` ignoring batch size when working with LOB (Large Object) columns.
51-
52-
- **Non-ASCII Path Resolution** - Fixed path resolution for files with non-ASCII characters on Windows.
53-
54-
### CI/Infrastructure
55-
56-
- **SQL Server 2025 Test Support** - Added support for testing against SQL Server 2025 across Windows, macOS, and Linux CI pipelines, ensuring driver compatibility with the upcoming SQL Server release.
57-
58-
- **Forked PR Coverage Workflow** - Implemented coverage comment workflow for pull requests from forked repositories, improving the contribution experience for external contributors.
42+
- **Segmentation Fault Fix** - Fixed segmentation fault in libmsodbcsql-18.5 during SQLFreeHandle() (#415).
5943

6044
For more information, please visit the project link on Github: https://github.com/microsoft/mssql-python
6145

eng/pipelines/pr-validation-pipeline.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ jobs:
5656

5757
strategy:
5858
matrix:
59+
pytestonwindows:
60+
# Temporary entry to clear stale CodeQL snapshot SM02986
61+
# Remove this once the issue is resolved
62+
sqlVersion: 'SQL2022'
63+
pythonVersion: '3.13'
5964
SQLServer2022:
6065
sqlVersion: 'SQL2022'
6166
pythonVersion: '3.13'

es-metadata.yml

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1-
schemaVersion: 0.0.1
2-
isProduction: true
3-
accountableOwners:
4-
service: ae66a2ba-2c8a-4e77-8323-305cfad11f0e
5-
routing:
6-
defaultAreaPath:
7-
org: sqlclientdrivers
8-
path: mssql-python
1+
schemaVersion: 1.0.0
2+
providers:
3+
- provider: InventoryAsCode
4+
version: 1.0.0
5+
metadata:
6+
isProduction: true
7+
accountableOwners:
8+
service: ae66a2ba-2c8a-4e77-8323-305cfad11f0e
9+
routing:
10+
defaultAreaPath:
11+
org: sqlclientdrivers
12+
path: mssql-python

mssql_python/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from .helpers import Settings, get_settings, _settings, _settings_lock
1616

1717
# Driver version
18-
__version__ = "1.2.0"
18+
__version__ = "1.3.0"
1919

2020
# Exceptions
2121
# https://www.python.org/dev/peps/pep-0249/#exceptions

mssql_python/pybind/connection/connection.cpp

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,47 @@ void Connection::connect(const py::dict& attrs_before) {
9494
void Connection::disconnect() {
9595
if (_dbcHandle) {
9696
LOG("Disconnecting from database");
97+
98+
// CRITICAL FIX: Mark all child statement handles as implicitly freed
99+
// When we free the DBC handle below, the ODBC driver will automatically free
100+
// all child STMT handles. We need to tell the SqlHandle objects about this
101+
// so they don't try to free the handles again during their destruction.
102+
103+
// THREAD-SAFETY: Lock mutex to safely access _childStatementHandles
104+
// This protects against concurrent allocStatementHandle() calls or GC finalizers
105+
{
106+
std::lock_guard<std::mutex> lock(_childHandlesMutex);
107+
108+
// First compact: remove expired weak_ptrs (they're already destroyed)
109+
size_t originalSize = _childStatementHandles.size();
110+
_childStatementHandles.erase(
111+
std::remove_if(_childStatementHandles.begin(), _childStatementHandles.end(),
112+
[](const std::weak_ptr<SqlHandle>& wp) { return wp.expired(); }),
113+
_childStatementHandles.end());
114+
115+
LOG("Compacted child handles: %zu -> %zu (removed %zu expired)",
116+
originalSize, _childStatementHandles.size(),
117+
originalSize - _childStatementHandles.size());
118+
119+
LOG("Marking %zu child statement handles as implicitly freed",
120+
_childStatementHandles.size());
121+
for (auto& weakHandle : _childStatementHandles) {
122+
if (auto handle = weakHandle.lock()) {
123+
// SAFETY ASSERTION: Only STMT handles should be in this vector
124+
// This is guaranteed by allocStatementHandle() which only creates STMT handles
125+
// If this assertion fails, it indicates a serious bug in handle tracking
126+
if (handle->type() != SQL_HANDLE_STMT) {
127+
LOG_ERROR("CRITICAL: Non-STMT handle (type=%d) found in _childStatementHandles. "
128+
"This will cause a handle leak!", handle->type());
129+
continue; // Skip marking to prevent leak
130+
}
131+
handle->markImplicitlyFreed();
132+
}
133+
}
134+
_childStatementHandles.clear();
135+
_allocationsSinceCompaction = 0;
136+
} // Release lock before potentially slow SQLDisconnect call
137+
97138
SQLRETURN ret = SQLDisconnect_ptr(_dbcHandle->get());
98139
checkError(ret);
99140
// triggers SQLFreeHandle via destructor, if last owner
@@ -173,7 +214,36 @@ SqlHandlePtr Connection::allocStatementHandle() {
173214
SQLHANDLE stmt = nullptr;
174215
SQLRETURN ret = SQLAllocHandle_ptr(SQL_HANDLE_STMT, _dbcHandle->get(), &stmt);
175216
checkError(ret);
176-
return std::make_shared<SqlHandle>(static_cast<SQLSMALLINT>(SQL_HANDLE_STMT), stmt);
217+
auto stmtHandle = std::make_shared<SqlHandle>(static_cast<SQLSMALLINT>(SQL_HANDLE_STMT), stmt);
218+
219+
// THREAD-SAFETY: Lock mutex before modifying _childStatementHandles
220+
// This protects against concurrent disconnect() or allocStatementHandle() calls,
221+
// or GC finalizers running from different threads
222+
{
223+
std::lock_guard<std::mutex> lock(_childHandlesMutex);
224+
225+
// Track this child handle so we can mark it as implicitly freed when connection closes
226+
// Use weak_ptr to avoid circular references and allow normal cleanup
227+
_childStatementHandles.push_back(stmtHandle);
228+
_allocationsSinceCompaction++;
229+
230+
// Compact expired weak_ptrs only periodically to avoid O(n²) overhead
231+
// This keeps allocation fast (O(1) amortized) while preventing unbounded growth
232+
// disconnect() also compacts, so this is just for long-lived connections with many cursors
233+
if (_allocationsSinceCompaction >= COMPACTION_INTERVAL) {
234+
size_t originalSize = _childStatementHandles.size();
235+
_childStatementHandles.erase(
236+
std::remove_if(_childStatementHandles.begin(), _childStatementHandles.end(),
237+
[](const std::weak_ptr<SqlHandle>& wp) { return wp.expired(); }),
238+
_childStatementHandles.end());
239+
_allocationsSinceCompaction = 0;
240+
LOG("Periodic compaction: %zu -> %zu handles (removed %zu expired)",
241+
originalSize, _childStatementHandles.size(),
242+
originalSize - _childStatementHandles.size());
243+
}
244+
} // Release lock
245+
246+
return stmtHandle;
177247
}
178248

179249
SQLRETURN Connection::setAttribute(SQLINTEGER attribute, py::object value) {
@@ -308,7 +378,7 @@ bool Connection::reset() {
308378
disconnect();
309379
return false;
310380
}
311-
381+
312382
// SQL_ATTR_RESET_CONNECTION does NOT reset the transaction isolation level.
313383
// Explicitly reset it to the default (SQL_TXN_READ_COMMITTED) to prevent
314384
// isolation level settings from leaking between pooled connection usages.
@@ -320,7 +390,7 @@ bool Connection::reset() {
320390
disconnect();
321391
return false;
322392
}
323-
393+
324394
updateLastUsed();
325395
return true;
326396
}

mssql_python/pybind/connection/connection.h

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,19 @@
55
#include "../ddbc_bindings.h"
66
#include <memory>
77
#include <string>
8+
#include <mutex>
89

910
// Represents a single ODBC database connection.
1011
// Manages connection handles.
1112
// Note: This class does NOT implement pooling logic directly.
13+
//
14+
// THREADING MODEL (per DB-API 2.0 threadsafety=1):
15+
// - Connections should NOT be shared between threads in normal usage
16+
// - However, _childStatementHandles is mutex-protected because:
17+
// 1. Python GC/finalizers can run from any thread
18+
// 2. Native code may release GIL during blocking ODBC calls
19+
// 3. Provides safety if user accidentally shares connection
20+
// - All accesses to _childStatementHandles are guarded by _childHandlesMutex
1221

1322
class Connection {
1423
public:
@@ -61,6 +70,22 @@ class Connection {
6170
std::chrono::steady_clock::time_point _lastUsed;
6271
std::wstring wstrStringBuffer; // wstr buffer for string attribute setting
6372
std::string strBytesBuffer; // string buffer for byte attributes setting
73+
74+
// Track child statement handles to mark them as implicitly freed when connection closes
75+
// Uses weak_ptr to avoid circular references and allow normal cleanup
76+
// THREAD-SAFETY: All accesses must be guarded by _childHandlesMutex
77+
std::vector<std::weak_ptr<SqlHandle>> _childStatementHandles;
78+
79+
// Counter for periodic compaction of expired weak_ptrs
80+
// Compact every N allocations to avoid O(n²) overhead in hot path
81+
// THREAD-SAFETY: Protected by _childHandlesMutex
82+
size_t _allocationsSinceCompaction = 0;
83+
static constexpr size_t COMPACTION_INTERVAL = 100;
84+
85+
// Mutex protecting _childStatementHandles and _allocationsSinceCompaction
86+
// Prevents data races between allocStatementHandle() and disconnect(),
87+
// or concurrent GC finalizers running from different threads
88+
mutable std::mutex _childHandlesMutex;
6489
};
6590

6691
class ConnectionHandle {

mssql_python/pybind/ddbc_bindings.cpp

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,6 +1144,21 @@ SQLSMALLINT SqlHandle::type() const {
11441144
return _type;
11451145
}
11461146

1147+
void SqlHandle::markImplicitlyFreed() {
1148+
// SAFETY: Only STMT handles should be marked as implicitly freed.
1149+
// When a DBC handle is freed, the ODBC driver automatically frees all child STMT handles.
1150+
// Other handle types (ENV, DBC, DESC) are NOT automatically freed by parents.
1151+
// Calling this on wrong handle types will cause silent handle leaks.
1152+
if (_type != SQL_HANDLE_STMT) {
1153+
// Log error but don't throw - we're likely in cleanup/destructor path
1154+
LOG_ERROR("SAFETY VIOLATION: Attempted to mark non-STMT handle as implicitly freed. "
1155+
"Handle type=%d. This will cause handle leak. Only STMT handles are "
1156+
"automatically freed by parent DBC handles.", _type);
1157+
return; // Refuse to mark - let normal free() handle it
1158+
}
1159+
_implicitly_freed = true;
1160+
}
1161+
11471162
/*
11481163
* IMPORTANT: Never log in destructors - it causes segfaults.
11491164
* During program exit, C++ destructors may run AFTER Python shuts down.
@@ -1169,16 +1184,19 @@ void SqlHandle::free() {
11691184
return;
11701185
}
11711186

1172-
// Always clean up ODBC resources, regardless of Python state
1187+
// CRITICAL FIX: Check if handle was already implicitly freed by parent handle
1188+
// When Connection::disconnect() frees the DBC handle, the ODBC driver automatically
1189+
// frees all child STMT handles. We track this state to avoid double-free attempts.
1190+
// This approach avoids calling ODBC functions on potentially-freed handles, which
1191+
// would cause use-after-free errors.
1192+
if (_implicitly_freed) {
1193+
_handle = nullptr; // Just clear the pointer, don't call ODBC functions
1194+
return;
1195+
}
1196+
1197+
// Handle is valid and not implicitly freed, proceed with normal freeing
11731198
SQLFreeHandle_ptr(_type, _handle);
11741199
_handle = nullptr;
1175-
1176-
// Only log if Python is not shutting down (to avoid segfault)
1177-
if (!pythonShuttingDown) {
1178-
// Don't log during destruction - even in normal cases it can be
1179-
// problematic If logging is needed, use explicit close() methods
1180-
// instead
1181-
}
11821200
}
11831201
}
11841202

@@ -2893,7 +2911,6 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p
28932911

28942912
// Cache decimal separator to avoid repeated system calls
28952913

2896-
28972914
for (SQLSMALLINT i = 1; i <= colCount; ++i) {
28982915
SQLWCHAR columnName[256];
28992916
SQLSMALLINT columnNameLen;
@@ -3615,8 +3632,6 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum
36153632
columnInfos[col].processedColumnSize + 1; // +1 for null terminator
36163633
}
36173634

3618-
3619-
36203635
// Performance: Build function pointer dispatch table (once per batch)
36213636
// This eliminates the switch statement from the hot loop - 10,000 rows × 10
36223637
// cols reduces from 100,000 switch evaluations to just 10 switch
@@ -4033,8 +4048,8 @@ SQLRETURN FetchMany_wrap(SqlHandlePtr StatementHandle, py::list& rows, int fetch
40334048
lobColumns.push_back(i + 1); // 1-based
40344049
}
40354050
}
4036-
4037-
// Initialized to 0 for LOB path counter; overwritten by ODBC in non-LOB path;
4051+
4052+
// Initialized to 0 for LOB path counter; overwritten by ODBC in non-LOB path;
40384053
SQLULEN numRowsFetched = 0;
40394054
// If we have LOBs → fall back to row-by-row fetch + SQLGetData_wrap
40404055
if (!lobColumns.empty()) {
@@ -4066,7 +4081,7 @@ SQLRETURN FetchMany_wrap(SqlHandlePtr StatementHandle, py::list& rows, int fetch
40664081
LOG("FetchMany_wrap: Error when binding columns - SQLRETURN=%d", ret);
40674082
return ret;
40684083
}
4069-
4084+
40704085
SQLSetStmtAttr_ptr(hStmt, SQL_ATTR_ROW_ARRAY_SIZE, (SQLPOINTER)(intptr_t)fetchSize, 0);
40714086
SQLSetStmtAttr_ptr(hStmt, SQL_ATTR_ROWS_FETCHED_PTR, &numRowsFetched, 0);
40724087

mssql_python/pybind/ddbc_bindings.h

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,9 +379,24 @@ class SqlHandle {
379379
SQLSMALLINT type() const;
380380
void free();
381381

382+
// Mark this handle as implicitly freed (freed by parent handle)
383+
// This prevents double-free attempts when the ODBC driver automatically
384+
// frees child handles (e.g., STMT handles when DBC handle is freed)
385+
//
386+
// SAFETY CONSTRAINTS:
387+
// - ONLY call this on SQL_HANDLE_STMT handles
388+
// - ONLY call this when the parent DBC handle is about to be freed
389+
// - Calling on other handle types (ENV, DBC, DESC) will cause HANDLE LEAKS
390+
// - The ODBC spec only guarantees automatic freeing of STMT handles by DBC parents
391+
//
392+
// Current usage: Connection::disconnect() marks all tracked STMT handles
393+
// before freeing the DBC handle.
394+
void markImplicitlyFreed();
395+
382396
private:
383397
SQLSMALLINT _type;
384398
SQLHANDLE _handle;
399+
bool _implicitly_freed = false; // Tracks if handle was freed by parent
385400
};
386401
using SqlHandlePtr = std::shared_ptr<SqlHandle>;
387402

0 commit comments

Comments
 (0)