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
30 changes: 29 additions & 1 deletion eng/pipelines/pr-validation-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -74,6 +77,7 @@ jobs:
inputs:
versionSpec: '$(pythonVersion)'
addToPath: true
allowUnstable: true
githubToken: $(GITHUB_TOKEN)
displayName: 'Use Python $(pythonVersion)'

Expand Down Expand Up @@ -582,6 +586,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
Expand All @@ -591,6 +599,7 @@ jobs:
inputs:
versionSpec: '$(pythonVersion)'
addToPath: true
allowUnstable: true
displayName: 'Use Python $(pythonVersion) on macOS'

- task: Cache@2
Expand Down Expand Up @@ -768,6 +777,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
Expand Down Expand Up @@ -797,7 +811,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
Comment on lines +819 to +821
"
elif [ "$(distroName)" = "Ubuntu" ]; then
docker exec test-container-$(distroName) bash -c "
export DEBIAN_FRONTEND=noninteractive
export TZ=UTC
Expand Down Expand Up @@ -866,6 +889,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" ] &&
Expand Down Expand Up @@ -1078,6 +1102,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: |
Expand Down
13 changes: 5 additions & 8 deletions mssql_python/cursor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2747,20 +2747,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.
Comment on lines 2749 to +2752
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)
Expand Down
48 changes: 48 additions & 0 deletions mssql_python/pybind/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -387,6 +394,47 @@ 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)
# 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)
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 <features.h>
#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
$<$<NOT:$<CONFIG:Debug>>:-U_FORTIFY_SOURCE>
$<$<NOT:$<CONFIG:Debug>>:-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")
Expand Down
11 changes: 7 additions & 4 deletions mssql_python/pybind/ddbc_bindings.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@

#pragma once

// pybind11.h must be the first include
#include <cstring>
#include <exception>
#include <memory>
// Python.h must precede standard-library headers so its feature-test macros
// are established before libc headers consume them.
#include <Python.h>
#include <pybind11/chrono.h>
#include <pybind11/complex.h>
#include <pybind11/functional.h>
#include <pybind11/pybind11.h>
#include <pybind11/pytypes.h> // Add this line for datetime support
#include <pybind11/stl.h>

#include <cstring>
#include <exception>
#include <memory>
#include <string>
#include <vector>

Expand Down
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
coverage
unittest-xml-reporting
psutil
pyarrow
--extra-index-url https://pypi.anaconda.org/scientific-python-nightly-wheels/simple
pyarrow; python_version < "3.15"
pyarrow==26.0.0.dev323; python_version >= "3.15"
polars

# Runtime dependencies needed for tests
Expand Down
8 changes: 7 additions & 1 deletion tests/test_008_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion tests/test_009_pooling.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -1369,6 +1368,15 @@ 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
assert not errors, errors
Expand Down
18 changes: 18 additions & 0 deletions tests/test_038_mssql_odbc_daily_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"


Expand Down Expand Up @@ -150,6 +151,23 @@ def test_stable_rs_transport_is_pinned(self):

self.assertEqual(version.strip(), "0.3.0")

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(" '\"")
for line in pipeline.splitlines()
if line.lstrip().startswith("pythonVersion:")
}

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):
requirements = (ROOT / "requirements.txt").read_text(encoding="utf-8")

self.assertIn("scientific-python-nightly-wheels", requirements)
self.assertIn('pyarrow==26.0.0.dev323; python_version >= "3.15"', requirements)


if __name__ == "__main__":
unittest.main()
Loading
Loading