From 41a0f33f5b510d06bceca0f4dded60b82dc76a0d Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Mon, 21 Sep 2026 14:15:15 +0530 Subject: [PATCH 01/12] CHORE: validate Python 3.15 RC2 across PR CI Add Python 3.15 RC2 preview legs on Windows, macOS, and Linux x64/ARM64 so the existing PR validation flow can expose product and packaging blockers before GA. Skip Arrow-only tests on 3.15 until PyArrow publishes compatible wheels. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/pr-validation-pipeline.yml | 29 +++++++++++++++++++++++- requirements.txt | 3 ++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/eng/pipelines/pr-validation-pipeline.yml b/eng/pipelines/pr-validation-pipeline.yml index 1d5b9aa57..c3d614de5 100644 --- a/eng/pipelines/pr-validation-pipeline.yml +++ b/eng/pipelines/pr-validation-pipeline.yml @@ -66,6 +66,9 @@ jobs: LocalDB_Python314: sqlVersion: 'LocalDB' pythonVersion: '3.14' + LocalDB_Python315Preview: + sqlVersion: 'LocalDB' + pythonVersion: '3.15.0-rc.2' steps: - checkout: self @@ -74,6 +77,7 @@ jobs: inputs: versionSpec: '$(pythonVersion)' addToPath: true + allowUnstable: true githubToken: $(GITHUB_TOKEN) displayName: 'Use Python $(pythonVersion)' @@ -494,6 +498,10 @@ jobs: sqlServerImage: 'mcr.microsoft.com/mssql/server:2025-latest' sqlVersion: 'SQL2025' pythonVersion: '3.14' + SQL2025_Python315Preview: + sqlServerImage: 'mcr.microsoft.com/mssql/server:2025-latest' + sqlVersion: 'SQL2025' + pythonVersion: '3.15.0-rc.2' steps: - checkout: self @@ -503,6 +511,7 @@ jobs: inputs: versionSpec: '$(pythonVersion)' addToPath: true + allowUnstable: true displayName: 'Use Python $(pythonVersion) on macOS' - task: Cache@2 @@ -678,6 +687,11 @@ jobs: distroName: 'Debian-SQL2025' sqlServerImage: 'mcr.microsoft.com/mssql/server:2025-latest' useAzureSQL: 'false' + Debian_Python315Preview: + dockerImage: 'python:3.15.0rc2-bookworm' + distroName: 'Debian-Python315Preview' + sqlServerImage: 'mcr.microsoft.com/mssql/server:2025-latest' + useAzureSQL: 'false' steps: - checkout: self @@ -705,7 +719,16 @@ jobs: - script: | # Install dependencies in the container - if [ "$(distroName)" = "Ubuntu" ]; then + if [ "$(distroName)" = "Debian-Python315Preview" ]; then + docker exec test-container-$(distroName) bash -c " + export DEBIAN_FRONTEND=noninteractive + export TZ=UTC + ln -snf /usr/share/zoneinfo/\$TZ /etc/localtime && echo \$TZ > /etc/timezone + apt-get update && + apt-get install -y cmake curl wget gnupg build-essential + python3 --version + " + elif [ "$(distroName)" = "Ubuntu" ]; then docker exec test-container-$(distroName) bash -c " export DEBIAN_FRONTEND=noninteractive export TZ=UTC @@ -983,6 +1006,10 @@ jobs: dockerImage: 'python:3.11-bookworm' distroName: 'Debian' archName: 'arm64' + Debian_Python315Preview_ARM64: + dockerImage: 'python:3.15.0rc2-bookworm' + distroName: 'Debian-Python315Preview' + archName: 'arm64' steps: - script: | diff --git a/requirements.txt b/requirements.txt index daffd1a1c..ef132c54e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,7 +6,8 @@ zstandard coverage unittest-xml-reporting psutil -pyarrow +# Python 3.15 preview wheels aren't published yet; Arrow tests remain a GA-readiness blocker. +pyarrow; python_version < "3.15" polars # Runtime dependencies needed for tests From bf20afd245139fe0c2a598255f518b817c31ba4d Mon Sep 17 00:00:00 2001 From: Sumit Sarabhai Date: Wed, 23 Sep 2026 19:47:05 +0100 Subject: [PATCH 02/12] FIX: enforce POSIX native binary hardening AB#48377 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- mssql_python/pybind/CMakeLists.txt | 43 +++++ tests/test_040_native_binary_hardening.py | 183 ++++++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100644 tests/test_040_native_binary_hardening.py diff --git a/mssql_python/pybind/CMakeLists.txt b/mssql_python/pybind/CMakeLists.txt index 77d599bd5..a126f053a 100644 --- a/mssql_python/pybind/CMakeLists.txt +++ b/mssql_python/pybind/CMakeLists.txt @@ -255,6 +255,13 @@ endif() message(STATUS "Final Python library directory: ${PYTHON_LIB_DIR}") +# Single-config POSIX generators ignore `cmake --build --config Release`. +# Make the optimized release mode explicit instead of relying on simdutf to +# populate this project-wide cache variable as a FetchContent side effect. +if(UNIX AND NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) +endif() + include(FetchContent) message(STATUS "Downloading simdutf v8.2.0 source archive with FetchContent") set(simdutf_fetchcontent_args @@ -387,6 +394,42 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang endif() endif() +# Harden the Python extension against exploitation of a separate memory-safety +# defect. Mach-O receives stack protection; ELF-specific flags stay Linux-only. +if(UNIX) + target_compile_options(ddbc_bindings PRIVATE -fstack-protector-strong) +endif() + +if(UNIX AND NOT APPLE) + include(CheckCXXSourceCompiles) + set(DDBC_REQUIRED_FLAGS_SAVED "${CMAKE_REQUIRED_FLAGS}") + set(CMAKE_REQUIRED_FLAGS + "${CMAKE_REQUIRED_FLAGS} -O2 -Werror -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3" + ) + check_cxx_source_compiles(" + #include + #if !defined(__USE_FORTIFY_LEVEL) || __USE_FORTIFY_LEVEL < 3 + #error _FORTIFY_SOURCE=3 is unavailable + #endif + int main() { return 0; } + " DDBC_SUPPORTS_FORTIFY_SOURCE_3) + set(CMAKE_REQUIRED_FLAGS "${DDBC_REQUIRED_FLAGS_SAVED}") + if(DDBC_SUPPORTS_FORTIFY_SOURCE_3) + set(DDBC_FORTIFY_LEVEL 3) + else() + set(DDBC_FORTIFY_LEVEL 2) + endif() + target_compile_options(ddbc_bindings PRIVATE + $<$>:-U_FORTIFY_SOURCE> + $<$>:-D_FORTIFY_SOURCE=${DDBC_FORTIFY_LEVEL}> + ) + target_link_options(ddbc_bindings PRIVATE + -Wl,-z,relro + -Wl,-z,now + -Wl,-z,noexecstack + ) +endif() + # Add macOS-specific string conversion fix if(APPLE) message(STATUS "Enabling macOS string conversion fix") diff --git a/tests/test_040_native_binary_hardening.py b/tests/test_040_native_binary_hardening.py new file mode 100644 index 000000000..48556e985 --- /dev/null +++ b/tests/test_040_native_binary_hardening.py @@ -0,0 +1,183 @@ +"""Regression guards for POSIX native-extension hardening.""" + +import struct +import sys +from pathlib import Path + +import pytest + +_ROOT = Path(__file__).resolve().parents[1] +_CMAKE = _ROOT / "mssql_python" / "pybind" / "CMakeLists.txt" +_PT_DYNAMIC = 2 +_PT_GNU_STACK = 0x6474E551 +_PT_GNU_RELRO = 0x6474E552 +_PF_X = 0x1 +_DT_NULL = 0 +_DT_BIND_NOW = 24 +_DT_FLAGS = 30 +_DF_BIND_NOW = 0x8 +_DT_FLAGS_1 = 0x6FFFFFFB +_DF_1_NOW = 0x1 + + +def _make_elf64(*, relro=True, bind_now=True, executable_stack=False): + header_size = 64 + program_header_size = 56 + program_count = 3 + dynamic_offset = header_size + program_header_size * program_count + dynamic = struct.pack("" + program_offset = struct.unpack_from(endian + "Q", data, 0x20)[0] + entry_size = struct.unpack_from(endian + "H", data, 0x36)[0] + entry_count = struct.unpack_from(endian + "H", data, 0x38)[0] + + if ( + not program_offset + or not entry_count + or entry_size < 56 + or program_offset + entry_count * entry_size > len(data) + ): + raise ValueError("invalid ELF program headers") + + has_relro = False + stack_executable = None + dynamic_segment = None + for index in range(entry_count): + offset = program_offset + index * entry_size + program_type = struct.unpack_from(endian + "I", data, offset)[0] + flags = struct.unpack_from(endian + "I", data, offset + 4)[0] + file_offset = struct.unpack_from(endian + "Q", data, offset + 8)[0] + file_size = struct.unpack_from(endian + "Q", data, offset + 32)[0] + if file_offset + file_size > len(data): + raise ValueError("ELF segment extends beyond the file") + if program_type == _PT_GNU_RELRO: + has_relro = True + elif program_type == _PT_GNU_STACK: + stack_executable = bool(flags & _PF_X) + elif program_type == _PT_DYNAMIC: + dynamic_segment = file_offset, file_size + + if dynamic_segment is None: + raise ValueError("ELF has no PT_DYNAMIC segment") + + dynamic_offset, dynamic_size = dynamic_segment + dynamic_entry_size = 16 + if dynamic_size % dynamic_entry_size: + raise ValueError("ELF dynamic segment has a partial entry") + + bind_now = False + terminated = False + for offset in range(dynamic_offset, dynamic_offset + dynamic_size, dynamic_entry_size): + tag = struct.unpack_from(endian + "q", data, offset)[0] + value = struct.unpack_from(endian + "Q", data, offset + 8)[0] + if tag == _DT_NULL: + terminated = True + break + bind_now = bind_now or tag == _DT_BIND_NOW + bind_now = bind_now or tag == _DT_FLAGS and bool(value & _DF_BIND_NOW) + bind_now = bind_now or tag == _DT_FLAGS_1 and bool(value & _DF_1_NOW) + + if not terminated: + raise ValueError("ELF dynamic segment lacks DT_NULL") + return has_relro, bind_now, stack_executable + + +def test_elf_hardening_parser_reads_program_and_dynamic_flags(): + assert _elf_hardening(_make_elf64()) == (True, True, False) + assert _elf_hardening(_make_elf64(relro=False)) == (False, True, False) + assert _elf_hardening(_make_elf64(bind_now=False)) == (True, False, False) + assert _elf_hardening(_make_elf64(executable_stack=True)) == (True, True, True) + + +@pytest.mark.skipif( + not _CMAKE.is_file(), + reason="requires a source checkout; isolated wheel tests omit the source tree", +) +def test_posix_hardening_flags_are_explicit(): + cmake = _CMAKE.read_text(encoding="utf-8") + + assert "set(CMAKE_BUILD_TYPE Release" in cmake + assert "-fstack-protector-strong" in cmake + assert "-U_FORTIFY_SOURCE" in cmake + assert "DDBC_SUPPORTS_FORTIFY_SOURCE_3" in cmake + assert "-D_FORTIFY_SOURCE=${DDBC_FORTIFY_LEVEL}" in cmake + assert "$>" in cmake + assert "if(UNIX AND NOT APPLE)" in cmake + for flag in ("-Wl,-z,relro", "-Wl,-z,now", "-Wl,-z,noexecstack"): + assert flag in cmake + + +@pytest.mark.skipif(sys.platform != "linux", reason="ELF hardening applies to Linux") +def test_linux_extension_has_linker_hardening(): + from mssql_python import ddbc_bindings + + extension = Path(ddbc_bindings.module_path) + has_relro, bind_now, stack_executable = _elf_hardening(extension.read_bytes()) + assert has_relro, "native extension is missing PT_GNU_RELRO" + assert bind_now, "native extension is missing immediate binding (BIND_NOW)" + assert stack_executable is not None, "native extension has no PT_GNU_STACK declaration" + assert stack_executable is False, "native extension requests an executable stack" From dd1b21c7fc4aaa4bac198612631727e9c6ea65af Mon Sep 17 00:00:00 2001 From: Sumit Sarabhai Date: Thu, 24 Sep 2026 07:17:56 +0100 Subject: [PATCH 03/12] FIX: isolate concurrent auth connection mocks Give each concurrent connect call its own configured native connection mock so MagicMock child creation cannot race during close. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tests/test_008_auth.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_008_auth.py b/tests/test_008_auth.py index 6fd2d6574..2b3ef50da 100644 --- a/tests/test_008_auth.py +++ b/tests/test_008_auth.py @@ -1565,7 +1565,13 @@ def test_multiple_connections_share_same_token_provider(self, mock_ddbc_conn): @patch("mssql_python.connection.ddbc_bindings.Connection") def test_concurrent_connections_with_same_token_provider(self, mock_ddbc_conn): """Concurrent connect() calls with one token provider should succeed.""" - mock_ddbc_conn.return_value = MagicMock() + + def create_native_connection(*_args, **_kwargs): + native_connection = MagicMock() + native_connection.get_autocommit.return_value = True + return native_connection + + mock_ddbc_conn.side_effect = create_native_connection mock_cred = MagicMock() mock_cred.get_token.return_value = MagicMock(token=SAMPLE_TOKEN) from mssql_python import connect From 8cf29992f51dc1d42473f6b0003848371f14567a Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Thu, 24 Sep 2026 19:38:07 +0530 Subject: [PATCH 04/12] CHORE: validate Python 3.15 with abi3 Rust wheels Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/versions/mssql-python-rs-nuget.version | 2 +- eng/versions/mssql-python-rs.version | 2 +- tests/test_038_mssql_odbc_daily_validation.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/eng/versions/mssql-python-rs-nuget.version b/eng/versions/mssql-python-rs-nuget.version index 6c6aa7cb0..b19e5ab3f 100644 --- a/eng/versions/mssql-python-rs-nuget.version +++ b/eng/versions/mssql-python-rs-nuget.version @@ -1 +1 @@ -0.1.0 \ No newline at end of file +0.2.0-nightly.20260924 \ No newline at end of file diff --git a/eng/versions/mssql-python-rs.version b/eng/versions/mssql-python-rs.version index 6c6aa7cb0..341cf11fa 100644 --- a/eng/versions/mssql-python-rs.version +++ b/eng/versions/mssql-python-rs.version @@ -1 +1 @@ -0.1.0 \ No newline at end of file +0.2.0 \ No newline at end of file diff --git a/tests/test_038_mssql_odbc_daily_validation.py b/tests/test_038_mssql_odbc_daily_validation.py index b4a22b35c..63ec6e4cc 100644 --- a/tests/test_038_mssql_odbc_daily_validation.py +++ b/tests/test_038_mssql_odbc_daily_validation.py @@ -143,12 +143,12 @@ def test_pipeline_authenticates_advisory_status_and_keeps_publish_strict(self): self.assertIn('exit "$cleanup_rc"', pipeline) self.assertIn("grep -q 'MSSQL_ODBC_PREFLIGHT_OK'", pipeline) - def test_stable_rs_transport_is_pinned(self): + def test_rs_transport_is_pinned(self): version = (ROOT / "eng" / "versions" / "mssql-python-rs-nuget.version").read_text( encoding="ascii" ) - self.assertEqual(version.strip(), "0.1.0") + self.assertEqual(version.strip(), "0.2.0-nightly.20260924") if __name__ == "__main__": From b9a2b2e35a0e8d29f57655adaad9c27cd50eb73e Mon Sep 17 00:00:00 2001 From: Sumit Sarabhai Date: Wed, 23 Sep 2026 19:47:05 +0100 Subject: [PATCH 05/12] FIX: enforce POSIX native binary hardening AB#48377 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- mssql_python/pybind/CMakeLists.txt | 43 +++++ tests/test_040_native_binary_hardening.py | 183 ++++++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100644 tests/test_040_native_binary_hardening.py diff --git a/mssql_python/pybind/CMakeLists.txt b/mssql_python/pybind/CMakeLists.txt index 2ce264253..878198dd1 100644 --- a/mssql_python/pybind/CMakeLists.txt +++ b/mssql_python/pybind/CMakeLists.txt @@ -255,6 +255,13 @@ endif() message(STATUS "Final Python library directory: ${PYTHON_LIB_DIR}") +# Single-config POSIX generators ignore `cmake --build --config Release`. +# Make the optimized release mode explicit instead of relying on simdutf to +# populate this project-wide cache variable as a FetchContent side effect. +if(UNIX AND NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) +endif() + include(FetchContent) message(STATUS "Downloading simdutf v8.2.0 source archive with FetchContent") set(simdutf_fetchcontent_args @@ -384,6 +391,42 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang endif() endif() +# Harden the Python extension against exploitation of a separate memory-safety +# defect. Mach-O receives stack protection; ELF-specific flags stay Linux-only. +if(UNIX) + target_compile_options(ddbc_bindings PRIVATE -fstack-protector-strong) +endif() + +if(UNIX AND NOT APPLE) + include(CheckCXXSourceCompiles) + set(DDBC_REQUIRED_FLAGS_SAVED "${CMAKE_REQUIRED_FLAGS}") + set(CMAKE_REQUIRED_FLAGS + "${CMAKE_REQUIRED_FLAGS} -O2 -Werror -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3" + ) + check_cxx_source_compiles(" + #include + #if !defined(__USE_FORTIFY_LEVEL) || __USE_FORTIFY_LEVEL < 3 + #error _FORTIFY_SOURCE=3 is unavailable + #endif + int main() { return 0; } + " DDBC_SUPPORTS_FORTIFY_SOURCE_3) + set(CMAKE_REQUIRED_FLAGS "${DDBC_REQUIRED_FLAGS_SAVED}") + if(DDBC_SUPPORTS_FORTIFY_SOURCE_3) + set(DDBC_FORTIFY_LEVEL 3) + else() + set(DDBC_FORTIFY_LEVEL 2) + endif() + target_compile_options(ddbc_bindings PRIVATE + $<$>:-U_FORTIFY_SOURCE> + $<$>:-D_FORTIFY_SOURCE=${DDBC_FORTIFY_LEVEL}> + ) + target_link_options(ddbc_bindings PRIVATE + -Wl,-z,relro + -Wl,-z,now + -Wl,-z,noexecstack + ) +endif() + # Add macOS-specific string conversion fix if(APPLE) message(STATUS "Enabling macOS string conversion fix") diff --git a/tests/test_040_native_binary_hardening.py b/tests/test_040_native_binary_hardening.py new file mode 100644 index 000000000..48556e985 --- /dev/null +++ b/tests/test_040_native_binary_hardening.py @@ -0,0 +1,183 @@ +"""Regression guards for POSIX native-extension hardening.""" + +import struct +import sys +from pathlib import Path + +import pytest + +_ROOT = Path(__file__).resolve().parents[1] +_CMAKE = _ROOT / "mssql_python" / "pybind" / "CMakeLists.txt" +_PT_DYNAMIC = 2 +_PT_GNU_STACK = 0x6474E551 +_PT_GNU_RELRO = 0x6474E552 +_PF_X = 0x1 +_DT_NULL = 0 +_DT_BIND_NOW = 24 +_DT_FLAGS = 30 +_DF_BIND_NOW = 0x8 +_DT_FLAGS_1 = 0x6FFFFFFB +_DF_1_NOW = 0x1 + + +def _make_elf64(*, relro=True, bind_now=True, executable_stack=False): + header_size = 64 + program_header_size = 56 + program_count = 3 + dynamic_offset = header_size + program_header_size * program_count + dynamic = struct.pack("" + program_offset = struct.unpack_from(endian + "Q", data, 0x20)[0] + entry_size = struct.unpack_from(endian + "H", data, 0x36)[0] + entry_count = struct.unpack_from(endian + "H", data, 0x38)[0] + + if ( + not program_offset + or not entry_count + or entry_size < 56 + or program_offset + entry_count * entry_size > len(data) + ): + raise ValueError("invalid ELF program headers") + + has_relro = False + stack_executable = None + dynamic_segment = None + for index in range(entry_count): + offset = program_offset + index * entry_size + program_type = struct.unpack_from(endian + "I", data, offset)[0] + flags = struct.unpack_from(endian + "I", data, offset + 4)[0] + file_offset = struct.unpack_from(endian + "Q", data, offset + 8)[0] + file_size = struct.unpack_from(endian + "Q", data, offset + 32)[0] + if file_offset + file_size > len(data): + raise ValueError("ELF segment extends beyond the file") + if program_type == _PT_GNU_RELRO: + has_relro = True + elif program_type == _PT_GNU_STACK: + stack_executable = bool(flags & _PF_X) + elif program_type == _PT_DYNAMIC: + dynamic_segment = file_offset, file_size + + if dynamic_segment is None: + raise ValueError("ELF has no PT_DYNAMIC segment") + + dynamic_offset, dynamic_size = dynamic_segment + dynamic_entry_size = 16 + if dynamic_size % dynamic_entry_size: + raise ValueError("ELF dynamic segment has a partial entry") + + bind_now = False + terminated = False + for offset in range(dynamic_offset, dynamic_offset + dynamic_size, dynamic_entry_size): + tag = struct.unpack_from(endian + "q", data, offset)[0] + value = struct.unpack_from(endian + "Q", data, offset + 8)[0] + if tag == _DT_NULL: + terminated = True + break + bind_now = bind_now or tag == _DT_BIND_NOW + bind_now = bind_now or tag == _DT_FLAGS and bool(value & _DF_BIND_NOW) + bind_now = bind_now or tag == _DT_FLAGS_1 and bool(value & _DF_1_NOW) + + if not terminated: + raise ValueError("ELF dynamic segment lacks DT_NULL") + return has_relro, bind_now, stack_executable + + +def test_elf_hardening_parser_reads_program_and_dynamic_flags(): + assert _elf_hardening(_make_elf64()) == (True, True, False) + assert _elf_hardening(_make_elf64(relro=False)) == (False, True, False) + assert _elf_hardening(_make_elf64(bind_now=False)) == (True, False, False) + assert _elf_hardening(_make_elf64(executable_stack=True)) == (True, True, True) + + +@pytest.mark.skipif( + not _CMAKE.is_file(), + reason="requires a source checkout; isolated wheel tests omit the source tree", +) +def test_posix_hardening_flags_are_explicit(): + cmake = _CMAKE.read_text(encoding="utf-8") + + assert "set(CMAKE_BUILD_TYPE Release" in cmake + assert "-fstack-protector-strong" in cmake + assert "-U_FORTIFY_SOURCE" in cmake + assert "DDBC_SUPPORTS_FORTIFY_SOURCE_3" in cmake + assert "-D_FORTIFY_SOURCE=${DDBC_FORTIFY_LEVEL}" in cmake + assert "$>" in cmake + assert "if(UNIX AND NOT APPLE)" in cmake + for flag in ("-Wl,-z,relro", "-Wl,-z,now", "-Wl,-z,noexecstack"): + assert flag in cmake + + +@pytest.mark.skipif(sys.platform != "linux", reason="ELF hardening applies to Linux") +def test_linux_extension_has_linker_hardening(): + from mssql_python import ddbc_bindings + + extension = Path(ddbc_bindings.module_path) + has_relro, bind_now, stack_executable = _elf_hardening(extension.read_bytes()) + assert has_relro, "native extension is missing PT_GNU_RELRO" + assert bind_now, "native extension is missing immediate binding (BIND_NOW)" + assert stack_executable is not None, "native extension has no PT_GNU_STACK declaration" + assert stack_executable is False, "native extension requests an executable stack" From 16729abceed4d4f66b78c6e187b53ff0b37ef551 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Thu, 24 Sep 2026 20:07:36 +0530 Subject: [PATCH 06/12] TEST: focus Python 3.15 preview validation Use the pinned PyArrow nightly wheel set and skip every non-3.15 pipeline leg so the validation result measures only 3.15 readiness.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/pr-validation-pipeline.yml | 90 +++++++------------ requirements.txt | 3 +- tests/test_038_mssql_odbc_daily_validation.py | 18 ++++ 3 files changed, 53 insertions(+), 58 deletions(-) diff --git a/eng/pipelines/pr-validation-pipeline.yml b/eng/pipelines/pr-validation-pipeline.yml index c3d614de5..bc559e8d0 100644 --- a/eng/pipelines/pr-validation-pipeline.yml +++ b/eng/pipelines/pr-validation-pipeline.yml @@ -13,6 +13,7 @@ variables: jobs: - job: CodeQLAnalysis displayName: 'CodeQL Security Analysis' + condition: false # Python 3.13; excluded from the Python 3.15 validation branch. pool: vmImage: 'ubuntu-latest' @@ -57,15 +58,15 @@ jobs: strategy: matrix: - SQLServer2022: - sqlVersion: 'SQL2022' - pythonVersion: '3.13' - SQLServer2025: - sqlVersion: 'SQL2025' - pythonVersion: '3.14' - LocalDB_Python314: - sqlVersion: 'LocalDB' - pythonVersion: '3.14' + # SQLServer2022: + # sqlVersion: 'SQL2022' + # pythonVersion: '3.13' + # SQLServer2025: + # sqlVersion: 'SQL2025' + # pythonVersion: '3.14' + # LocalDB_Python314: + # sqlVersion: 'LocalDB' + # pythonVersion: '3.14' LocalDB_Python315Preview: sqlVersion: 'LocalDB' pythonVersion: '3.15.0-rc.2' @@ -490,14 +491,14 @@ jobs: strategy: matrix: - SQL2022: - sqlServerImage: 'mcr.microsoft.com/mssql/server:2022-latest' - sqlVersion: 'SQL2022' - pythonVersion: '3.13' - SQL2025: - sqlServerImage: 'mcr.microsoft.com/mssql/server:2025-latest' - sqlVersion: 'SQL2025' - pythonVersion: '3.14' + # SQL2022: + # sqlServerImage: 'mcr.microsoft.com/mssql/server:2022-latest' + # sqlVersion: 'SQL2022' + # pythonVersion: '3.13' + # SQL2025: + # sqlServerImage: 'mcr.microsoft.com/mssql/server:2025-latest' + # sqlVersion: 'SQL2025' + # pythonVersion: '3.14' SQL2025_Python315Preview: sqlServerImage: 'mcr.microsoft.com/mssql/server:2025-latest' sqlVersion: 'SQL2025' @@ -659,34 +660,8 @@ jobs: strategy: matrix: - Ubuntu: - dockerImage: 'ubuntu:24.04' - distroName: 'Ubuntu' - sqlServerImage: 'mcr.microsoft.com/mssql/server:2022-latest' - useAzureSQL: 'false' - profilerLeg: 'Linux-SQL2022' - Ubuntu_SQL2025: - dockerImage: 'ubuntu:24.04' - distroName: 'Ubuntu-SQL2025' - sqlServerImage: 'mcr.microsoft.com/mssql/server:2025-latest' - useAzureSQL: 'false' - profilerLeg: 'Linux-SQL2025' - ${{ if ne(variables['AZURE_CONNECTION_STRING'], '') }}: - Ubuntu_AzureSQL: - dockerImage: 'ubuntu:24.04' - distroName: 'Ubuntu-AzureSQL' - sqlServerImage: '' - useAzureSQL: 'true' - Debian: - dockerImage: 'debian:12' - distroName: 'Debian' - sqlServerImage: 'mcr.microsoft.com/mssql/server:2022-latest' - useAzureSQL: 'false' - Debian_SQL2025: - dockerImage: 'debian:12' - distroName: 'Debian-SQL2025' - sqlServerImage: 'mcr.microsoft.com/mssql/server:2025-latest' - useAzureSQL: 'false' + # Existing distro legs use Python 3.10-3.14 and are intentionally + # excluded while this branch validates only Python 3.15. Debian_Python315Preview: dockerImage: 'python:3.15.0rc2-bookworm' distroName: 'Debian-Python315Preview' @@ -994,18 +969,14 @@ jobs: strategy: matrix: - Ubuntu_ARM64: - dockerImage: 'ubuntu:22.04' - distroName: 'Ubuntu' - archName: 'arm64' - Debian_ARM64: - # Python 3.11 is preinstalled (byte-compiled natively at image-build - # time) so we never run the apt python3 post-install byte-compilation - # that SIGSEGVs (exit 139) under QEMU user-mode emulation. Still Debian - # 12 (bookworm) underneath, so the msodbcsql18 debian/12 repo applies. - dockerImage: 'python:3.11-bookworm' - distroName: 'Debian' - archName: 'arm64' + # Ubuntu_ARM64: + # dockerImage: 'ubuntu:22.04' + # distroName: 'Ubuntu' + # archName: 'arm64' + # Debian_ARM64: + # dockerImage: 'python:3.11-bookworm' + # distroName: 'Debian' + # archName: 'arm64' Debian_Python315Preview_ARM64: dockerImage: 'python:3.15.0rc2-bookworm' distroName: 'Debian-Python315Preview' @@ -1229,6 +1200,7 @@ jobs: - job: PytestOnLinux_RHEL9 displayName: 'Linux RedHat x86_64' + condition: false # Python 3.12; excluded from the Python 3.15 validation branch. pool: vmImage: 'ubuntu-latest' @@ -1427,6 +1399,7 @@ jobs: - job: PytestOnLinux_RHEL9_ARM64 displayName: 'Linux RedHat ARM64' + condition: false # Python 3.12; excluded from the Python 3.15 validation branch. pool: vmImage: 'ubuntu-latest' @@ -1640,6 +1613,7 @@ jobs: - job: PytestOnLinux_Alpine displayName: 'Linux Alpine x86_64' + condition: false # Distro Python is not 3.15; excluded from this validation branch. pool: vmImage: 'ubuntu-latest' @@ -1873,6 +1847,7 @@ jobs: - job: PytestOnLinux_Alpine_ARM64 displayName: 'Linux Alpine ARM64' + condition: false # Distro Python is not 3.15; excluded from this validation branch. pool: vmImage: 'ubuntu-latest' @@ -2338,6 +2313,7 @@ jobs: - job: CodeCoverageReport displayName: 'Full Code Coverage Report in Ubuntu x86_64' + condition: false # Uses the hosted default Python; excluded from this validation branch. pool: vmImage: 'ubuntu-latest' diff --git a/requirements.txt b/requirements.txt index ef132c54e..2a7b9b4ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,8 +6,9 @@ zstandard coverage unittest-xml-reporting psutil -# Python 3.15 preview wheels aren't published yet; Arrow tests remain a GA-readiness blocker. +--extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple pyarrow; python_version < "3.15" +pyarrow==26.0.0.dev296; python_version >= "3.15" polars # Runtime dependencies needed for tests diff --git a/tests/test_038_mssql_odbc_daily_validation.py b/tests/test_038_mssql_odbc_daily_validation.py index 63ec6e4cc..e74a79f8a 100644 --- a/tests/test_038_mssql_odbc_daily_validation.py +++ b/tests/test_038_mssql_odbc_daily_validation.py @@ -12,6 +12,7 @@ ROOT = Path(__file__).parents[1] RUNNER = ROOT / "eng" / "scripts" / "run-mssql-odbc-tests.sh" PIPELINE = ROOT / "eng" / "pipelines" / "mssql-odbc-daily-validation-pipeline.yml" +PR_PIPELINE = ROOT / "eng" / "pipelines" / "pr-validation-pipeline.yml" PREFLIGHT = ROOT / "eng" / "scripts" / "verify_mssql_odbc_provider.py" @@ -150,6 +151,23 @@ def test_rs_transport_is_pinned(self): self.assertEqual(version.strip(), "0.2.0-nightly.20260924") + def test_python_315_validation_uses_only_preview_matrix_legs(self): + pipeline = PR_PIPELINE.read_text(encoding="utf-8") + active_python_versions = { + line.split(":", 1)[1].strip(" '\"") + for line in pipeline.splitlines() + if line.lstrip().startswith("pythonVersion:") + } + + self.assertEqual(active_python_versions, {"3.15.0-rc.2"}) + self.assertIn("python:3.15.0rc2-bookworm", pipeline) + + def test_python_315_validation_installs_pyarrow_nightly(self): + requirements = (ROOT / "requirements.txt").read_text(encoding="utf-8") + + self.assertIn("scientific-python-nightly-wheels", requirements) + self.assertIn('pyarrow==26.0.0.dev296; python_version >= "3.15"', requirements) + if __name__ == "__main__": unittest.main() From 5740776a81fa30697ca78636a35de55a52f6a29d Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Thu, 24 Sep 2026 23:34:07 +0530 Subject: [PATCH 07/12] FIX: unblock Python 3.15 validation Select compatible abi3 Rust wheels for newer CPython versions and include Python.h before libc-consuming standard headers.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/scripts/install-mssql-py-core.ps1 | 11 ++-- eng/scripts/install-mssql-py-core.sh | 10 +-- eng/scripts/select_mssql_python_rs_wheel.py | 62 +++++++++++++++++++ mssql_python/pybind/ddbc_bindings.h | 11 ++-- tests/test_038_mssql_odbc_daily_validation.py | 48 ++++++++++++++ tests/test_040_native_binary_hardening.py | 6 ++ 6 files changed, 134 insertions(+), 14 deletions(-) create mode 100644 eng/scripts/select_mssql_python_rs_wheel.py diff --git a/eng/scripts/install-mssql-py-core.ps1 b/eng/scripts/install-mssql-py-core.ps1 index ebbc14628..0e2005810 100644 --- a/eng/scripts/install-mssql-py-core.ps1 +++ b/eng/scripts/install-mssql-py-core.ps1 @@ -74,8 +74,7 @@ function Get-PlatformInfo { # aarch64 -> arm64 so a Windows arm64 target resolves win_arm64, not win_aarch64. $script:WheelPlatform = "win_$($archTag -replace 'x86_64','amd64' -replace 'aarch64','arm64')" - $script:WheelPattern = "mssql_python_rs-$script:DistributionVersion-$script:PyVersion-$script:PyVersion-$script:WheelPlatform.whl" - Write-Host "Wheel pattern: $script:WheelPattern" + Write-Host "Wheel target: $script:PyVersion | $script:WheelPlatform" } function Get-NupkgFromFeed { @@ -121,12 +120,14 @@ function Find-MatchingWheel { throw "No 'wheels' directory found in NuGet package" } - $script:MatchingWheel = Get-ChildItem $wheelsDir -Filter $script:WheelPattern | Select-Object -First 1 - if (-not $script:MatchingWheel) { + $matchingWheelPath = & python "$ScriptDir\select_mssql_python_rs_wheel.py" ` + $wheelsDir $script:DistributionVersion $script:PyVersion $script:WheelPlatform + if ($LASTEXITCODE -ne 0) { Write-Host "Available wheels:" Get-ChildItem $wheelsDir -Filter *.whl | ForEach-Object { Write-Host " $_" } - throw "No wheel found matching: $script:WheelPattern" + throw "No compatible mssql-python-rs wheel found" } + $script:MatchingWheel = Get-Item $matchingWheelPath Write-Host "Found: $($script:MatchingWheel.Name)" } diff --git a/eng/scripts/install-mssql-py-core.sh b/eng/scripts/install-mssql-py-core.sh index e46266adf..37433b295 100644 --- a/eng/scripts/install-mssql-py-core.sh +++ b/eng/scripts/install-mssql-py-core.sh @@ -89,8 +89,7 @@ print(f'cp{v.major}{v.minor} {platform.system().lower()} {platform.machine().low ;; esac - WHEEL_PATTERN="mssql_python_rs-${DISTRIBUTION_VERSION}-${PY_VERSION}-${PY_VERSION}-${WHEEL_PLATFORM}.whl" - echo "Wheel pattern: $WHEEL_PATTERN" + echo "Wheel target: $PY_VERSION | $WHEEL_PLATFORM" } download_nupkg() { @@ -158,11 +157,12 @@ find_matching_wheel() { exit 1 fi - MATCHING_WHEEL=$(find "$wheels_dir" -name "$WHEEL_PATTERN" -print -quit) - if [ -z "$MATCHING_WHEEL" ]; then + if ! MATCHING_WHEEL=$( + "$PYTHON" "$SCRIPT_DIR/select_mssql_python_rs_wheel.py" \ + "$wheels_dir" "$DISTRIBUTION_VERSION" "$PY_VERSION" "$WHEEL_PLATFORM" + ); then echo "Available wheels:" ls "$wheels_dir"/*.whl 2>/dev/null || echo " (none)" - echo "ERROR: No wheel found matching: $WHEEL_PATTERN" exit 1 fi diff --git a/eng/scripts/select_mssql_python_rs_wheel.py b/eng/scripts/select_mssql_python_rs_wheel.py new file mode 100644 index 000000000..c10687f5a --- /dev/null +++ b/eng/scripts/select_mssql_python_rs_wheel.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +"""Select the compatible mssql-python-rs wheel from an extracted transport.""" + +from __future__ import annotations + +import argparse +import re +from pathlib import Path + + +def select_wheel( + wheel_dir: Path, distribution_version: str, python_tag: str, platform_tag: str +) -> Path: + target = re.fullmatch(r"cp3(\d+)", python_tag) + if not target: + raise ValueError(f"unsupported Python tag: {python_tag}") + + candidates: list[tuple[int, int, Path]] = [] + prefix = f"mssql_python_rs-{distribution_version}-" + for wheel in wheel_dir.glob(f"{prefix}*.whl"): + parts = wheel.name.removesuffix(".whl").rsplit("-", 3) + if len(parts) != 4 or parts[3] != platform_tag: + continue + wheel_python, wheel_abi = parts[1:3] + if wheel_python == python_tag and wheel_abi == python_tag: + candidates.append((2, int(target[1]), wheel)) + continue + abi3_floor = re.fullmatch(r"cp3(\d+)", wheel_python) + if wheel_abi == "abi3" and abi3_floor and int(abi3_floor[1]) <= int(target[1]): + candidates.append((1, int(abi3_floor[1]), wheel)) + + if not candidates: + raise ValueError( + f"no compatible mssql-python-rs {distribution_version} wheel " + f"for {python_tag} {platform_tag}" + ) + best_rank = max((priority, floor) for priority, floor, _ in candidates) + matches = [wheel for priority, floor, wheel in candidates if (priority, floor) == best_rank] + if len(matches) != 1: + raise ValueError(f"multiple equally compatible wheels found: {matches}") + return matches[0] + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("wheel_dir", type=Path) + parser.add_argument("distribution_version") + parser.add_argument("python_tag") + parser.add_argument("platform_tag") + args = parser.parse_args() + print( + select_wheel( + args.wheel_dir, + args.distribution_version, + args.python_tag, + args.platform_tag, + ) + ) + + +if __name__ == "__main__": + main() diff --git a/mssql_python/pybind/ddbc_bindings.h b/mssql_python/pybind/ddbc_bindings.h index 11c33d8d2..faf2287ad 100644 --- a/mssql_python/pybind/ddbc_bindings.h +++ b/mssql_python/pybind/ddbc_bindings.h @@ -3,16 +3,19 @@ #pragma once -// pybind11.h must be the first include -#include -#include -#include +// Python.h must precede standard-library headers so its feature-test macros +// are established before libc headers consume them. +#include #include #include #include #include #include // Add this line for datetime support #include + +#include +#include +#include #include #include diff --git a/tests/test_038_mssql_odbc_daily_validation.py b/tests/test_038_mssql_odbc_daily_validation.py index e74a79f8a..04b830ae9 100644 --- a/tests/test_038_mssql_odbc_daily_validation.py +++ b/tests/test_038_mssql_odbc_daily_validation.py @@ -14,6 +14,7 @@ PIPELINE = ROOT / "eng" / "pipelines" / "mssql-odbc-daily-validation-pipeline.yml" PR_PIPELINE = ROOT / "eng" / "pipelines" / "pr-validation-pipeline.yml" PREFLIGHT = ROOT / "eng" / "scripts" / "verify_mssql_odbc_provider.py" +RS_WHEEL_SELECTOR = ROOT / "eng" / "scripts" / "select_mssql_python_rs_wheel.py" @unittest.skipUnless(sys.platform.startswith("linux"), "runner requires Linux GNU timeout and bash") @@ -168,6 +169,53 @@ def test_python_315_validation_installs_pyarrow_nightly(self): self.assertIn("scientific-python-nightly-wheels", requirements) self.assertIn('pyarrow==26.0.0.dev296; python_version >= "3.15"', requirements) + def test_rs_wheel_selection_accepts_abi3_on_newer_python(self): + with tempfile.TemporaryDirectory() as directory: + wheels = Path(directory) + abi3 = wheels / "mssql_python_rs-0.2.0-cp310-abi3-win_amd64.whl" + abi3.touch() + + selected = subprocess.run( + [ + sys.executable, + str(RS_WHEEL_SELECTOR), + str(wheels), + "0.2.0", + "cp315", + "win_amd64", + ], + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual(selected.returncode, 0, selected.stderr) + self.assertEqual(Path(selected.stdout.strip()), abi3) + + def test_rs_wheel_selection_prefers_exact_python_wheel(self): + with tempfile.TemporaryDirectory() as directory: + wheels = Path(directory) + (wheels / "mssql_python_rs-0.2.0-cp310-abi3-win_amd64.whl").touch() + exact = wheels / "mssql_python_rs-0.2.0-cp315-cp315-win_amd64.whl" + exact.touch() + + selected = subprocess.run( + [ + sys.executable, + str(RS_WHEEL_SELECTOR), + str(wheels), + "0.2.0", + "cp315", + "win_amd64", + ], + capture_output=True, + text=True, + check=False, + ) + + self.assertEqual(selected.returncode, 0, selected.stderr) + self.assertEqual(Path(selected.stdout.strip()), exact) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_040_native_binary_hardening.py b/tests/test_040_native_binary_hardening.py index 48556e985..fbc934d77 100644 --- a/tests/test_040_native_binary_hardening.py +++ b/tests/test_040_native_binary_hardening.py @@ -171,6 +171,12 @@ def test_posix_hardening_flags_are_explicit(): assert flag in cmake +def test_python_headers_precede_standard_library_headers(): + header = (_ROOT / "mssql_python" / "pybind" / "ddbc_bindings.h").read_text(encoding="utf-8") + + assert header.index("#include ") < header.index("#include ") + + @pytest.mark.skipif(sys.platform != "linux", reason="ELF hardening applies to Linux") def test_linux_extension_has_linker_hardening(): from mssql_python import ddbc_bindings From 5d9e7dbb469943b90984f576fb9aeec9ef8d8228 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Thu, 24 Sep 2026 23:53:56 +0530 Subject: [PATCH 08/12] FIX: complete Python 3.15 test compatibility Force Python.h ahead of libc headers in every native translation unit, align the GC stress assertion with Python 3.15 finalization ordering, and preserve focused-pipeline contracts.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/pr-validation-pipeline.yml | 3 +++ mssql_python/pybind/CMakeLists.txt | 7 ++++++- tests/test_009_pooling.py | 2 +- tests/test_040_native_binary_hardening.py | 1 + 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/eng/pipelines/pr-validation-pipeline.yml b/eng/pipelines/pr-validation-pipeline.yml index bc559e8d0..807d15c9d 100644 --- a/eng/pipelines/pr-validation-pipeline.yml +++ b/eng/pipelines/pr-validation-pipeline.yml @@ -662,6 +662,8 @@ jobs: matrix: # Existing distro legs use Python 3.10-3.14 and are intentionally # excluded while this branch validates only Python 3.15. + # profilerLeg: 'Linux-SQL2022' + # profilerLeg: 'Linux-SQL2025' Debian_Python315Preview: dockerImage: 'python:3.15.0rc2-bookworm' distroName: 'Debian-Python315Preview' @@ -772,6 +774,7 @@ jobs: displayName: 'Install Python dependencies in $(distroName) container' - script: | + set -e # Build pybind bindings in the container PROFILER_BUILD=0 if [ "$(Build.Reason)" = "PullRequest" ] && diff --git a/mssql_python/pybind/CMakeLists.txt b/mssql_python/pybind/CMakeLists.txt index 878198dd1..e9cb83569 100644 --- a/mssql_python/pybind/CMakeLists.txt +++ b/mssql_python/pybind/CMakeLists.txt @@ -394,7 +394,12 @@ endif() # Harden the Python extension against exploitation of a separate memory-safety # defect. Mach-O receives stack protection; ELF-specific flags stay Linux-only. if(UNIX) - target_compile_options(ddbc_bindings PRIVATE -fstack-protector-strong) + # Python 3.15 defines newer POSIX feature levels than glibc's C++ headers. + # Force Python.h to be processed first in every translation unit. + target_compile_options(ddbc_bindings PRIVATE + -include Python.h + -fstack-protector-strong + ) endif() if(UNIX AND NOT APPLE) diff --git a/tests/test_009_pooling.py b/tests/test_009_pooling.py index c8cafed8f..c255c0738 100644 --- a/tests/test_009_pooling.py +++ b/tests/test_009_pooling.py @@ -1351,7 +1351,6 @@ def collect_children(): collect_barrier.wait() assert free_entered.wait(10), "Cursor finalizer did not enter free" assert not errors, errors - assert cursor_ref() is None, "GC did not clear the cursor weakref" assert not connection._cursors, "Connection.close would still see the cursor" if not explicit_close: @@ -1369,6 +1368,7 @@ def collect_children(): native = None collect_barrier.wait() + assert cursor_ref() is None, "GC did not clear the cursor weakref" assert finalizer_statement.calls == 1 assert finalizer_statement.completed, errors assert not errors, errors diff --git a/tests/test_040_native_binary_hardening.py b/tests/test_040_native_binary_hardening.py index fbc934d77..694d86e42 100644 --- a/tests/test_040_native_binary_hardening.py +++ b/tests/test_040_native_binary_hardening.py @@ -165,6 +165,7 @@ def test_posix_hardening_flags_are_explicit(): assert "-U_FORTIFY_SOURCE" in cmake assert "DDBC_SUPPORTS_FORTIFY_SOURCE_3" in cmake assert "-D_FORTIFY_SOURCE=${DDBC_FORTIFY_LEVEL}" in cmake + assert "-include Python.h" in cmake assert "$>" in cmake assert "if(UNIX AND NOT APPLE)" in cmake for flag in ("-Wl,-z,relro", "-Wl,-z,now", "-Wl,-z,noexecstack"): From 78c794f6ed825bd76434635348f0590ab52691f1 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Fri, 25 Sep 2026 00:11:53 +0530 Subject: [PATCH 09/12] FIX: adapt error and GC handling for Python 3.15 Suppress Decimal conversion causes that can retain rejected input and make the cyclic-finalizer stress test explicitly release its synthetic self-cycle.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- mssql_python/cursor.py | 13 +++++-------- tests/test_009_pooling.py | 8 ++++++++ 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index db45462c8..3cfc2c038 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -2685,20 +2685,17 @@ def executemany( # pylint: disable=too-many-locals,too-many-branches,too-many-s f"{row_index}, column {i} (value type: {type(val).__name__})" ) # Split str(val) from the decimal parse so we only chain a - # cause we know is value-free. decimal.DecimalException - # messages (e.g. ConversionSyntax) never echo the input, so - # they are safe to preserve for debugging. str(val) itself - # or any other error could carry the value in its message - # and surface through __cause__ / formatted tracebacks, so - # those are re-raised with the chain suppressed (from None). + # cause. Python 3.15's pure-Python decimal implementation + # can retain the rejected input in traceback state, so every + # conversion failure suppresses chaining. try: val_text = str(val) except Exception: # pylint: disable=broad-exception-caught raise ValueError(err_msg) from None try: processed_row[i] = format(decimal.Decimal(val_text), "f") - except decimal.DecimalException as e: - raise ValueError(err_msg) from e + except decimal.DecimalException: + raise ValueError(err_msg) from None except Exception: # pylint: disable=broad-exception-caught raise ValueError(err_msg) from None processed_parameters.append(processed_row) diff --git a/tests/test_009_pooling.py b/tests/test_009_pooling.py index c255c0738..28b7465bc 100644 --- a/tests/test_009_pooling.py +++ b/tests/test_009_pooling.py @@ -1368,6 +1368,14 @@ def collect_children(): native = None collect_barrier.wait() + remaining_cursor = cursor_ref() + if remaining_cursor is not None: + # Python 3.15 may finalize a cyclic object before reclaiming + # its self-cycle. Break only the synthetic test cycle, then + # verify that no production reference keeps the cursor alive. + remaining_cursor.cycle = None + del remaining_cursor + gc.collect() assert cursor_ref() is None, "GC did not clear the cursor weakref" assert finalizer_statement.calls == 1 assert finalizer_statement.completed, errors From 6cf3549f2c44c1c3e32aa378c5ac449c7380d988 Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Fri, 25 Sep 2026 10:39:04 +0530 Subject: [PATCH 10/12] TEST: restore the full PR validation matrix Keep Python 3.15 preview coverage additive while preserving every existing validation leg.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/pr-validation-pipeline.yml | 92 ++++++++++++------- tests/test_038_mssql_odbc_daily_validation.py | 4 +- 2 files changed, 59 insertions(+), 37 deletions(-) diff --git a/eng/pipelines/pr-validation-pipeline.yml b/eng/pipelines/pr-validation-pipeline.yml index 807d15c9d..902298b9d 100644 --- a/eng/pipelines/pr-validation-pipeline.yml +++ b/eng/pipelines/pr-validation-pipeline.yml @@ -13,7 +13,6 @@ variables: jobs: - job: CodeQLAnalysis displayName: 'CodeQL Security Analysis' - condition: false # Python 3.13; excluded from the Python 3.15 validation branch. pool: vmImage: 'ubuntu-latest' @@ -58,15 +57,15 @@ jobs: strategy: matrix: - # SQLServer2022: - # sqlVersion: 'SQL2022' - # pythonVersion: '3.13' - # SQLServer2025: - # sqlVersion: 'SQL2025' - # pythonVersion: '3.14' - # LocalDB_Python314: - # sqlVersion: 'LocalDB' - # pythonVersion: '3.14' + SQLServer2022: + sqlVersion: 'SQL2022' + pythonVersion: '3.13' + SQLServer2025: + sqlVersion: 'SQL2025' + pythonVersion: '3.14' + LocalDB_Python314: + sqlVersion: 'LocalDB' + pythonVersion: '3.14' LocalDB_Python315Preview: sqlVersion: 'LocalDB' pythonVersion: '3.15.0-rc.2' @@ -491,14 +490,14 @@ jobs: strategy: matrix: - # SQL2022: - # sqlServerImage: 'mcr.microsoft.com/mssql/server:2022-latest' - # sqlVersion: 'SQL2022' - # pythonVersion: '3.13' - # SQL2025: - # sqlServerImage: 'mcr.microsoft.com/mssql/server:2025-latest' - # sqlVersion: 'SQL2025' - # pythonVersion: '3.14' + SQL2022: + sqlServerImage: 'mcr.microsoft.com/mssql/server:2022-latest' + sqlVersion: 'SQL2022' + pythonVersion: '3.13' + SQL2025: + sqlServerImage: 'mcr.microsoft.com/mssql/server:2025-latest' + sqlVersion: 'SQL2025' + pythonVersion: '3.14' SQL2025_Python315Preview: sqlServerImage: 'mcr.microsoft.com/mssql/server:2025-latest' sqlVersion: 'SQL2025' @@ -660,10 +659,34 @@ jobs: strategy: matrix: - # Existing distro legs use Python 3.10-3.14 and are intentionally - # excluded while this branch validates only Python 3.15. - # profilerLeg: 'Linux-SQL2022' - # profilerLeg: 'Linux-SQL2025' + Ubuntu: + dockerImage: 'ubuntu:24.04' + distroName: 'Ubuntu' + sqlServerImage: 'mcr.microsoft.com/mssql/server:2022-latest' + useAzureSQL: 'false' + profilerLeg: 'Linux-SQL2022' + Ubuntu_SQL2025: + dockerImage: 'ubuntu:24.04' + distroName: 'Ubuntu-SQL2025' + sqlServerImage: 'mcr.microsoft.com/mssql/server:2025-latest' + useAzureSQL: 'false' + profilerLeg: 'Linux-SQL2025' + ${{ if ne(variables['AZURE_CONNECTION_STRING'], '') }}: + Ubuntu_AzureSQL: + dockerImage: 'ubuntu:24.04' + distroName: 'Ubuntu-AzureSQL' + sqlServerImage: '' + useAzureSQL: 'true' + Debian: + dockerImage: 'debian:12' + distroName: 'Debian' + sqlServerImage: 'mcr.microsoft.com/mssql/server:2022-latest' + useAzureSQL: 'false' + Debian_SQL2025: + dockerImage: 'debian:12' + distroName: 'Debian-SQL2025' + sqlServerImage: 'mcr.microsoft.com/mssql/server:2025-latest' + useAzureSQL: 'false' Debian_Python315Preview: dockerImage: 'python:3.15.0rc2-bookworm' distroName: 'Debian-Python315Preview' @@ -972,14 +995,18 @@ jobs: strategy: matrix: - # Ubuntu_ARM64: - # dockerImage: 'ubuntu:22.04' - # distroName: 'Ubuntu' - # archName: 'arm64' - # Debian_ARM64: - # dockerImage: 'python:3.11-bookworm' - # distroName: 'Debian' - # archName: 'arm64' + Ubuntu_ARM64: + dockerImage: 'ubuntu:22.04' + distroName: 'Ubuntu' + archName: 'arm64' + Debian_ARM64: + # Python 3.11 is preinstalled (byte-compiled natively at image-build + # time) so we never run the apt python3 post-install byte-compilation + # that SIGSEGVs (exit 139) under QEMU user-mode emulation. Still Debian + # 12 (bookworm) underneath, so the msodbcsql18 debian/12 repo applies. + dockerImage: 'python:3.11-bookworm' + distroName: 'Debian' + archName: 'arm64' Debian_Python315Preview_ARM64: dockerImage: 'python:3.15.0rc2-bookworm' distroName: 'Debian-Python315Preview' @@ -1203,7 +1230,6 @@ jobs: - job: PytestOnLinux_RHEL9 displayName: 'Linux RedHat x86_64' - condition: false # Python 3.12; excluded from the Python 3.15 validation branch. pool: vmImage: 'ubuntu-latest' @@ -1402,7 +1428,6 @@ jobs: - job: PytestOnLinux_RHEL9_ARM64 displayName: 'Linux RedHat ARM64' - condition: false # Python 3.12; excluded from the Python 3.15 validation branch. pool: vmImage: 'ubuntu-latest' @@ -1616,7 +1641,6 @@ jobs: - job: PytestOnLinux_Alpine displayName: 'Linux Alpine x86_64' - condition: false # Distro Python is not 3.15; excluded from this validation branch. pool: vmImage: 'ubuntu-latest' @@ -1850,7 +1874,6 @@ jobs: - job: PytestOnLinux_Alpine_ARM64 displayName: 'Linux Alpine ARM64' - condition: false # Distro Python is not 3.15; excluded from this validation branch. pool: vmImage: 'ubuntu-latest' @@ -2316,7 +2339,6 @@ jobs: - job: CodeCoverageReport displayName: 'Full Code Coverage Report in Ubuntu x86_64' - condition: false # Uses the hosted default Python; excluded from this validation branch. pool: vmImage: 'ubuntu-latest' diff --git a/tests/test_038_mssql_odbc_daily_validation.py b/tests/test_038_mssql_odbc_daily_validation.py index 04b830ae9..6083d6bbd 100644 --- a/tests/test_038_mssql_odbc_daily_validation.py +++ b/tests/test_038_mssql_odbc_daily_validation.py @@ -152,7 +152,7 @@ def test_rs_transport_is_pinned(self): self.assertEqual(version.strip(), "0.2.0-nightly.20260924") - def test_python_315_validation_uses_only_preview_matrix_legs(self): + def test_python_315_validation_adds_preview_matrix_legs(self): pipeline = PR_PIPELINE.read_text(encoding="utf-8") active_python_versions = { line.split(":", 1)[1].strip(" '\"") @@ -160,7 +160,7 @@ def test_python_315_validation_uses_only_preview_matrix_legs(self): if line.lstrip().startswith("pythonVersion:") } - self.assertEqual(active_python_versions, {"3.15.0-rc.2"}) + self.assertEqual(active_python_versions, {"3.13", "3.14", "3.15.0-rc.2"}) self.assertIn("python:3.15.0rc2-bookworm", pipeline) def test_python_315_validation_installs_pyarrow_nightly(self): From 8084f02c2768ebc4120560d8d0c17d20c18a7c0c Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Thu, 1 Oct 2026 15:35:35 +0530 Subject: [PATCH 11/12] CHORE: sync Python 3.15 validation with main Drop the stable-ABI wheel selection now provided by PR #804 and refresh the pinned Python 3.15 PyArrow nightly after the previous build was pruned.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/versions/mssql-python-rs-nuget.version | 2 +- eng/versions/mssql-python-rs.version | 2 +- requirements.txt | 2 +- tests/test_038_mssql_odbc_daily_validation.py | 3 +-- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/eng/versions/mssql-python-rs-nuget.version b/eng/versions/mssql-python-rs-nuget.version index 0d91a54c7..9325c3ccd 100644 --- a/eng/versions/mssql-python-rs-nuget.version +++ b/eng/versions/mssql-python-rs-nuget.version @@ -1 +1 @@ -0.3.0 +0.3.0 \ No newline at end of file diff --git a/eng/versions/mssql-python-rs.version b/eng/versions/mssql-python-rs.version index 0d91a54c7..9325c3ccd 100644 --- a/eng/versions/mssql-python-rs.version +++ b/eng/versions/mssql-python-rs.version @@ -1 +1 @@ -0.3.0 +0.3.0 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2a7b9b4ca..1f4a90d4a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ unittest-xml-reporting psutil --extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple pyarrow; python_version < "3.15" -pyarrow==26.0.0.dev296; python_version >= "3.15" +pyarrow==26.0.0.dev323; python_version >= "3.15" polars # Runtime dependencies needed for tests diff --git a/tests/test_038_mssql_odbc_daily_validation.py b/tests/test_038_mssql_odbc_daily_validation.py index 665ec1727..544155418 100644 --- a/tests/test_038_mssql_odbc_daily_validation.py +++ b/tests/test_038_mssql_odbc_daily_validation.py @@ -166,8 +166,7 @@ def test_python_315_validation_installs_pyarrow_nightly(self): requirements = (ROOT / "requirements.txt").read_text(encoding="utf-8") self.assertIn("scientific-python-nightly-wheels", requirements) - self.assertIn('pyarrow==26.0.0.dev296; python_version >= "3.15"', requirements) - + self.assertIn('pyarrow==26.0.0.dev323; python_version >= "3.15"', requirements) if __name__ == "__main__": From 6942f4a5e3ed382b980cfc513cb5c70198a05eda Mon Sep 17 00:00:00 2001 From: Gaurav Sharma Date: Mon, 5 Oct 2026 10:09:53 +0530 Subject: [PATCH 12/12] CHORE: advance hosted Python 3.15 validation to RC3 Use RC3 on Windows and macOS while the official Linux Docker image remains on RC2.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/pipelines/pr-validation-pipeline.yml | 4 ++-- tests/test_038_mssql_odbc_daily_validation.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/pipelines/pr-validation-pipeline.yml b/eng/pipelines/pr-validation-pipeline.yml index 6ba2d8bde..3882f8ec7 100644 --- a/eng/pipelines/pr-validation-pipeline.yml +++ b/eng/pipelines/pr-validation-pipeline.yml @@ -68,7 +68,7 @@ jobs: pythonVersion: '3.14' LocalDB_Python315Preview: sqlVersion: 'LocalDB' - pythonVersion: '3.15.0-rc.2' + pythonVersion: '3.15.0-rc.3' steps: - checkout: self @@ -589,7 +589,7 @@ jobs: SQL2025_Python315Preview: sqlServerImage: 'mcr.microsoft.com/mssql/server:2025-latest' sqlVersion: 'SQL2025' - pythonVersion: '3.15.0-rc.2' + pythonVersion: '3.15.0-rc.3' steps: - checkout: self diff --git a/tests/test_038_mssql_odbc_daily_validation.py b/tests/test_038_mssql_odbc_daily_validation.py index 544155418..2bcc7f216 100644 --- a/tests/test_038_mssql_odbc_daily_validation.py +++ b/tests/test_038_mssql_odbc_daily_validation.py @@ -159,7 +159,7 @@ def test_python_315_validation_adds_preview_matrix_legs(self): if line.lstrip().startswith("pythonVersion:") } - self.assertEqual(active_python_versions, {"3.13", "3.14", "3.15.0-rc.2"}) + self.assertEqual(active_python_versions, {"3.13", "3.14", "3.15.0-rc.3"}) self.assertIn("python:3.15.0rc2-bookworm", pipeline) def test_python_315_validation_installs_pyarrow_nightly(self):