Skip to content

Commit 6fd72e6

Browse files
committed
Merged PR 6157: SYNC: Github main to ADO main
#### AI description (iteration 1) #### PR Classification This PR implements API enhancements and extensive test/pipeline improvements to strengthen metadata, output conversion, encoding/decoding, and batch execution features. #### PR Summary - **`eng/pipelines/build-whl-pipeline.yml` & `.github/workflows/pr-code-coverage.yml`:** Updated to build wheels and report code coverage across multi‑architecture Linux containers (manylinux, musllinux, Alpine, etc.). - **`mssql_python/cursor.py`:** Improved metadata handling and result set processing with new helper methods and fallback descriptions. - **`mssql_python/connection.py`:** Added a robust output converter API (including methods to add, get, remove, and clear converters) and enhanced `execute`/`batch_execute` with better error and timeout management. - **Tests in `/tests/`:** Expanded coverage for connection, encoding/decoding, output converters, rowcount, transaction handling, and thread safety to meet DB‑API 2.0 requirements. - **File deletion:** Removed `mssql_python/testing_ddbc_bindings.py` as its functionality has been integrated into the main modules. <!-- GitOpsUserAgent=GitOps.Apps.Server.pullrequestcopilot --> Related work items: #39062
1 parent ec764d1 commit 6fd72e6

File tree

7 files changed

+622
-31
lines changed

7 files changed

+622
-31
lines changed

PyPI_Description.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,12 @@ PyBind11 provides:
3939

4040
We are currently in **Public Preview**.
4141

42-
## What's new in v0.12.0
42+
## What's new in v0.13.0
4343

44-
- **Complex Data Type Support:** Added native support for DATETIMEOFFSET and UNIQUEIDENTIFIER data types with full round-trip handling, enabling seamless integration with Python's timezone-aware `datetime` objects and `uuid.UUID` types.
45-
- **Support for monetary or currency values data types:** Extended MONEY and SMALLMONEY support to `executemany` operations with proper NULL handling and decimal conversion for improved bulk financial data processing.
46-
- **Improved Database Metadata API:** Added `getinfo()` method with enhanced ODBC metadata retrieval, allowing users to query driver/data source information using ODBC info types.
47-
- **Data Processing Optimizations:** Removed aggressive datetime parsing to prevent incorrect type conversions and improve data integrity across diverse datetime formats and string data.
44+
- **Enhanced Batch Operations:** Complete support for UNIQUEIDENTIFIER and DATETIMEOFFSET in `executemany()` operations with automatic type inference, enabling efficient bulk inserts of complex data types including UUIDs and timezone-aware datetimes.
45+
- **Streaming Large Values:** Robust handling of large objects (NVARCHAR/VARCHAR/VARBINARY(MAX)) in `executemany()` with automatic Data-At-Execution detection and fallback, supporting streaming inserts and fetches for massive datasets.
46+
- **Improved Cursor Reliability:** Enhanced `cursor.rowcount` accuracy across all fetch operations, including proper handling of empty result sets and consistent behavior for SELECT, INSERT, and UPDATE operations.
47+
- **Critical Stability Fixes:** Resolved memory leaks with secure token buffer handling, fixed resource cleanup to prevent segmentation faults during Python shutdown, and corrected type inference bugs in batch operations.
4848

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

mssql_python/cursor.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1613,6 +1613,7 @@ def executemany(self, operation: str, seq_of_parameters: list) -> None:
16131613
# Use auto-detection for columns without explicit types
16141614
column = [row[col_index] for row in seq_of_parameters] if hasattr(seq_of_parameters, '__getitem__') else []
16151615
sample_value, min_val, max_val = self._compute_column_type(column)
1616+
16161617
dummy_row = list(sample_row)
16171618
paraminfo = self._create_parameter_types_list(
16181619
sample_value, param_info, dummy_row, col_index, min_val=min_val, max_val=max_val
@@ -1724,6 +1725,10 @@ def fetchone(self) -> Union[None, Row]:
17241725
self.messages.extend(ddbc_bindings.DDBCSQLGetAllDiagRecords(self.hstmt))
17251726

17261727
if ret == ddbc_sql_const.SQL_NO_DATA.value:
1728+
# No more data available
1729+
if self._next_row_index == 0 and self.description is not None:
1730+
# This is an empty result set, set rowcount to 0
1731+
self.rowcount = 0
17271732
return None
17281733

17291734
# Update internal position after successful fetch
@@ -1732,6 +1737,8 @@ def fetchone(self) -> Union[None, Row]:
17321737
self._next_row_index += 1
17331738
else:
17341739
self._increment_rownumber()
1740+
1741+
self.rowcount = self._next_row_index
17351742

17361743
# Create and return a Row object, passing column name map if available
17371744
column_map = getattr(self, '_column_name_map', None)
@@ -1774,6 +1781,12 @@ def fetchmany(self, size: int = None) -> List[Row]:
17741781
# advance counters by number of rows actually returned
17751782
self._next_row_index += len(rows_data)
17761783
self._rownumber = self._next_row_index - 1
1784+
1785+
# Centralize rowcount assignment after fetch
1786+
if len(rows_data) == 0 and self._next_row_index == 0:
1787+
self.rowcount = 0
1788+
else:
1789+
self.rowcount = self._next_row_index
17771790

17781791
# Convert raw data to Row objects
17791792
column_map = getattr(self, '_column_name_map', None)
@@ -1806,6 +1819,12 @@ def fetchall(self) -> List[Row]:
18061819
if rows_data and self._has_result_set:
18071820
self._next_row_index += len(rows_data)
18081821
self._rownumber = self._next_row_index - 1
1822+
1823+
# Centralize rowcount assignment after fetch
1824+
if len(rows_data) == 0 and self._next_row_index == 0:
1825+
self.rowcount = 0
1826+
else:
1827+
self.rowcount = self._next_row_index
18091828

18101829
# Convert raw data to Row objects
18111830
column_map = getattr(self, '_column_name_map', None)

mssql_python/pybind/connection/connection.cpp

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,16 +173,16 @@ SQLRETURN Connection::setAttribute(SQLINTEGER attribute, py::object value) {
173173
LOG("Setting SQL attribute");
174174
SQLPOINTER ptr = nullptr;
175175
SQLINTEGER length = 0;
176+
std::string buffer; // to hold sensitive data temporarily
176177

177178
if (py::isinstance<py::int_>(value)) {
178179
int intValue = value.cast<int>();
179180
ptr = reinterpret_cast<SQLPOINTER>(static_cast<uintptr_t>(intValue));
180181
length = SQL_IS_INTEGER;
181182
} else if (py::isinstance<py::bytes>(value) || py::isinstance<py::bytearray>(value)) {
182-
static std::vector<std::string> buffers;
183-
buffers.emplace_back(value.cast<std::string>());
184-
ptr = const_cast<char*>(buffers.back().c_str());
185-
length = static_cast<SQLINTEGER>(buffers.back().size());
183+
buffer = value.cast<std::string>(); // stack buffer
184+
ptr = buffer.data();
185+
length = static_cast<SQLINTEGER>(buffer.size());
186186
} else {
187187
LOG("Unsupported attribute value type");
188188
return SQL_ERROR;
@@ -195,6 +195,11 @@ SQLRETURN Connection::setAttribute(SQLINTEGER attribute, py::object value) {
195195
else {
196196
LOG("Set attribute successfully");
197197
}
198+
199+
// Zero out sensitive data if used
200+
if (!buffer.empty()) {
201+
std::fill(buffer.begin(), buffer.end(), static_cast<char>(0));
202+
}
198203
return ret;
199204
}
200205

mssql_python/pybind/ddbc_bindings.cpp

Lines changed: 83 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -665,24 +665,67 @@ void HandleZeroColumnSizeAtFetch(SQLULEN& columnSize) {
665665

666666
} // namespace
667667

668+
// Helper function to check if Python is shutting down or finalizing
669+
// This centralizes the shutdown detection logic to avoid code duplication
670+
static bool is_python_finalizing() {
671+
try {
672+
if (Py_IsInitialized() == 0) {
673+
return true; // Python is already shut down
674+
}
675+
676+
py::gil_scoped_acquire gil;
677+
py::object sys_module = py::module_::import("sys");
678+
if (!sys_module.is_none()) {
679+
// Check if the attribute exists before accessing it (for Python version compatibility)
680+
if (py::hasattr(sys_module, "_is_finalizing")) {
681+
py::object finalizing_func = sys_module.attr("_is_finalizing");
682+
if (!finalizing_func.is_none() && finalizing_func().cast<bool>()) {
683+
return true; // Python is finalizing
684+
}
685+
}
686+
}
687+
return false;
688+
} catch (...) {
689+
std::cerr << "Error occurred while checking Python finalization state." << std::endl;
690+
// Be conservative - don't assume shutdown on any exception
691+
// Only return true if we're absolutely certain Python is shutting down
692+
return false;
693+
}
694+
}
695+
668696
// TODO: Revisit GIL considerations if we're using python's logger
669697
template <typename... Args>
670698
void LOG(const std::string& formatString, Args&&... args) {
671-
py::gil_scoped_acquire gil; // <---- this ensures safe Python API usage
699+
// Check if Python is shutting down to avoid crash during cleanup
700+
if (is_python_finalizing()) {
701+
return; // Python is shutting down or finalizing, don't log
702+
}
703+
704+
try {
705+
py::gil_scoped_acquire gil; // <---- this ensures safe Python API usage
672706

673-
py::object logger = py::module_::import("mssql_python.logging_config").attr("get_logger")();
674-
if (py::isinstance<py::none>(logger)) return;
707+
py::object logger = py::module_::import("mssql_python.logging_config").attr("get_logger")();
708+
if (py::isinstance<py::none>(logger)) return;
675709

676-
try {
677-
std::string ddbcFormatString = "[DDBC Bindings log] " + formatString;
678-
if constexpr (sizeof...(args) == 0) {
679-
logger.attr("debug")(py::str(ddbcFormatString));
680-
} else {
681-
py::str message = py::str(ddbcFormatString).format(std::forward<Args>(args)...);
682-
logger.attr("debug")(message);
710+
try {
711+
std::string ddbcFormatString = "[DDBC Bindings log] " + formatString;
712+
if constexpr (sizeof...(args) == 0) {
713+
logger.attr("debug")(py::str(ddbcFormatString));
714+
} else {
715+
py::str message = py::str(ddbcFormatString).format(std::forward<Args>(args)...);
716+
logger.attr("debug")(message);
717+
}
718+
} catch (const std::exception& e) {
719+
std::cerr << "Logging error: " << e.what() << std::endl;
683720
}
721+
} catch (const py::error_already_set& e) {
722+
// Python is shutting down or in an inconsistent state, silently ignore
723+
(void)e; // Suppress unused variable warning
724+
return;
684725
} catch (const std::exception& e) {
685-
std::cerr << "Logging error: " << e.what() << std::endl;
726+
// Any other error, ignore to prevent crash during cleanup
727+
(void)e; // Suppress unused variable warning
728+
return;
686729
}
687730
}
688731

@@ -993,17 +1036,26 @@ SQLSMALLINT SqlHandle::type() const {
9931036
*/
9941037
void SqlHandle::free() {
9951038
if (_handle && SQLFreeHandle_ptr) {
996-
const char* type_str = nullptr;
997-
switch (_type) {
998-
case SQL_HANDLE_ENV: type_str = "ENV"; break;
999-
case SQL_HANDLE_DBC: type_str = "DBC"; break;
1000-
case SQL_HANDLE_STMT: type_str = "STMT"; break;
1001-
case SQL_HANDLE_DESC: type_str = "DESC"; break;
1002-
default: type_str = "UNKNOWN"; break;
1039+
// Check if Python is shutting down using centralized helper function
1040+
bool pythonShuttingDown = is_python_finalizing();
1041+
1042+
// CRITICAL FIX: During Python shutdown, don't free STMT handles as their parent DBC may already be freed
1043+
// This prevents segfault when handles are freed in wrong order during interpreter shutdown
1044+
// Type 3 = SQL_HANDLE_STMT, Type 2 = SQL_HANDLE_DBC, Type 1 = SQL_HANDLE_ENV
1045+
if (pythonShuttingDown && _type == 3) {
1046+
_handle = nullptr; // Mark as freed to prevent double-free attempts
1047+
return;
10031048
}
1049+
1050+
// Always clean up ODBC resources, regardless of Python state
10041051
SQLFreeHandle_ptr(_type, _handle);
10051052
_handle = nullptr;
1006-
// Don't log during destruction - it can cause segfaults during Python shutdown
1053+
1054+
// Only log if Python is not shutting down (to avoid segfault)
1055+
if (!pythonShuttingDown) {
1056+
// Don't log during destruction - even in normal cases it can be problematic
1057+
// If logging is needed, use explicit close() methods instead
1058+
}
10071059
}
10081060
}
10091061

@@ -2017,8 +2069,10 @@ SQLRETURN BindParameterArray(SQLHANDLE hStmt,
20172069
SQLGUID* guidArray = AllocateParamBufferArray<SQLGUID>(tempBuffers, paramSetSize);
20182070
strLenOrIndArray = AllocateParamBufferArray<SQLLEN>(tempBuffers, paramSetSize);
20192071

2020-
static py::module_ uuid_mod = py::module_::import("uuid");
2021-
static py::object uuid_class = uuid_mod.attr("UUID");
2072+
// Get cached UUID class from module-level helper
2073+
// This avoids static object destruction issues during Python finalization
2074+
py::object uuid_class = py::module_::import("mssql_python.ddbc_bindings").attr("_get_uuid_class")();
2075+
20222076
for (size_t i = 0; i < paramSetSize; ++i) {
20232077
const py::handle& element = columnValues[i];
20242078
std::array<unsigned char, 16> uuid_bytes;
@@ -3851,6 +3905,14 @@ PYBIND11_MODULE(ddbc_bindings, m) {
38513905
});
38523906

38533907

3908+
// Module-level UUID class cache
3909+
// This caches the uuid.UUID class at module initialization time and keeps it alive
3910+
// for the entire module lifetime, avoiding static destructor issues during Python finalization
3911+
m.def("_get_uuid_class", []() -> py::object {
3912+
static py::object uuid_class = py::module_::import("uuid").attr("UUID");
3913+
return uuid_class;
3914+
}, "Internal helper to get cached UUID class");
3915+
38543916
// Add a version attribute
38553917
m.attr("__version__") = "1.0.0";
38563918

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def finalize_options(self):
8383

8484
setup(
8585
name='mssql-python',
86-
version='0.12.0',
86+
version='0.13.0',
8787
description='A Python library for interacting with Microsoft SQL Server',
8888
long_description=open('PyPI_Description.md', encoding='utf-8').read(),
8989
long_description_content_type='text/markdown',

0 commit comments

Comments
 (0)