Skip to content

FEAT: Add opt-in/opt-out ODBC provider selection (msodbcsql18 / mssql-odbc) - #730

Open
gargsaumya wants to merge 10 commits into
mainfrom
saumya/rust-odbc-optin
Open

FEAT: Add opt-in/opt-out ODBC provider selection (msodbcsql18 / mssql-odbc)#730
gargsaumya wants to merge 10 commits into
mainfrom
saumya/rust-odbc-optin

Conversation

@gargsaumya

@gargsaumya gargsaumya commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Linked work item: AB#47445

Summary

Introduces runtime selection between the classic Microsoft ODBC Driver 18 (msodbcsql18) and the Rust ODBC driver (mssql-odbc), without changing the public API or install command. msodbcsql18 remains the default (Phase 1); the Rust driver is opt-in and fails closed until its provider wheel ships.

Motivation

Design-review feedback on the Rust ODBC provider integration: hardcode the two providers and switch on an env var + module property, and use customer-friendly provider strings msodbcsql18 / mssql-odbc (not "classic").

What's implemented

  • Selection engine (mssql_python/odbc_provider.py, ProviderManager): precedence MSSQL_PYTHON_ODBC_PROVIDER env var -> mssql_python.odbc_provider module property -> default (msodbcsql18); values normalized and validated; unknown value fails closed (no fallback).
  • Resolve-once / freeze at first connect; a change to the module property after load is ignored with a RuntimeWarning (connection-pool model).
  • Public surface: settable mssql_python.odbc_provider property + get_odbc_provider_info() diagnostic.
  • Freeze wiring (connection.py, pooling.py): Connection.__init__ and PoolingManager.enable() both resolve + verify the provider (ensure_available()), then push the selection to native (ddbc_bindings.set_odbc_provider) before the driver loads, so an explicit pooling() call before any connect() still honors the selection.
  • Native loader switch (pybind/ddbc_bindings.cpp): Python is the sole resolver; the native side only reads the value pushed via set_odbc_provider(), falling back to the hardcoded classic default if nothing was pushed yet (no environment variable read in C++). GetOdbcLibsBaseDir imports the selected provider's package; GetDriverPathCpp has a Rust branch (mssql-odbc.dll/.so/.dylib, no lib prefix, under the mssql-python-owned libs/ layout); Windows mssql-auth.dll is non-fatal for the Rust provider. Classic path is unchanged.

Behavior

  • Default -> classic driver loads exactly as today.
  • mssql-odbc selected with its package absent -> clear fail-closed error: "...package 'mssql_python_rust_odbc' is not installed. Install it with: pip install mssql-python-rust-odbc." No dummy wheels required.

Testing

  • New tests/test_026_odbc_provider.py — 17 unit tests (precedence, normalization, fail-closed, freeze, post-freeze warning, diagnostics, public surface).
  • Native extension rebuilt (x64/cp313); full suite run against a live SQL Server: 2311 passed, 56 skipped, 0 failed.
  • Verified real default-provider connect end-to-end, and env-driven mssql-odbc fail-closed behavior end-to-end.

Deferred (tracked separately)

  • Rust ODBC readiness (missing exports) — mssql-rs.
  • Native artifact build + mssql-python-rust-odbc wheel pipeline -> PyPI dependency.
  • Running mssql-python's suite against the Rust driver in the mssql-rs pipeline.
  • Phase 2: flip default to mssql-odbc.

Resolve the ODBC provider from MSSQL_PYTHON_ODBC_PROVIDER env var, the mssql_python.odbc_provider module property, then a default (msodbcsql18). Selection resolves once and freezes at first connect; unknown values fail closed. The native loader imports the selected provider package and resolves a provider-specific driver path. Adds get_odbc_provider_info() diagnostics and unit tests.
Comment thread mssql_python/pybind/ddbc_bindings.cpp Fixed
@gargsaumya
gargsaumya changed the base branch from main to saumya/bump-py-core-0.1.9 August 26, 2026 07:41
@gargsaumya
gargsaumya changed the base branch from saumya/bump-py-core-0.1.9 to main August 26, 2026 08:50
@gargsaumya
gargsaumya marked this pull request as ready for review August 26, 2026 08:50
Copilot AI lite review requested due to automatic review settings August 26, 2026 08:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a process-wide, resolve-once ODBC provider selection mechanism so mssql-python can switch at runtime between the classic ODBC Driver 18 provider (msodbcsql18) and a future Rust provider (mssql-odbc), while keeping the existing public connection API unchanged.

Changes:

  • Introduces ProviderManager to resolve provider selection (env var → module property → default), freeze it at first resolution, and fail closed on invalid selections.
  • Wires provider resolution into Connection.__init__ and pushes the selected provider into the native loader via ddbc_bindings.set_odbc_provider.
  • Adds a new unit test module covering precedence/normalization/freeze behavior and missing-provider fail-closed behavior.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/test_026_odbc_provider.py Adds unit tests for provider precedence, normalization, freeze semantics, and fail-closed behavior.
mssql_python/pybind/ddbc_bindings.cpp Adds native-side provider selection plumbing and uses provider-specific package/dist names during driver resolution.
mssql_python/odbc_provider.py Implements the Python-side provider selection engine (ProviderManager).
mssql_python/connection.py Freezes/verifies provider selection and pushes it into the native layer before driver load.
mssql_python/init.py Exposes mssql_python.odbc_provider and get_odbc_provider_info() as public diagnostics/surface area.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tests/test_026_odbc_provider.py
Comment thread mssql_python/pybind/ddbc_bindings.cpp Outdated
Comment thread mssql_python/__init__.py
Comment thread mssql_python/__init__.py
Comment thread mssql_python/pybind/ddbc_bindings.cpp
@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown

📊 Code Coverage Report

🔥 Diff Coverage

97%


🎯 Overall Coverage

82%


📈 Total Lines Covered: 7917 out of 9593
📁 Project: mssql-python


Diff Coverage

Diff: main...HEAD, staged and unstaged changes

  • mssql_python/init.py (100%)
  • mssql_python/connection.py (100%)
  • mssql_python/odbc_provider.py (100%)
  • mssql_python/pooling.py (100%)
  • mssql_python/pybind/ddbc_bindings.cpp (93.3%): Missing lines 1087,1089-1090

Summary

  • Total: 145 lines
  • Missing: 3 lines
  • Coverage: 97%

mssql_python/pybind/ddbc_bindings.cpp

Lines 1083-1094

  1083         if (e.matches(PyExc_ModuleNotFoundError)) {
  1084             // Phase 2: the standalone package is required. Turn the missing
  1085             // dependency into a clear, actionable error instead of a fallback.
  1086             LOG("GetOdbcLibsBaseDir: required package %s is not installed (%s)",
! 1087                 packageName.c_str(), e.what());
  1088             ThrowStdException(
! 1089                 "The required '" + distName + "' package (which ships the ODBC driver "
! 1090                 "binaries) is not installed. Install it with: pip install " + distName);
  1091         }
  1092         // A different import-time error means the package is installed but
  1093         // broken; surface it instead of silently masking the real problem.
  1094         LOG("GetOdbcLibsBaseDir: importing %s failed unexpectedly (%s); "


📋 Files Needing Attention

📉 Files with overall lowest coverage (click to expand)
mssql_python.pybind.logger_bridge.cpp: 59.2%
mssql_python.pybind.ddbc_bindings.h: 61.5%
mssql_python.pybind.logger_bridge.hpp: 70.8%
mssql_python.pybind.ddbc_bindings.cpp: 75.6%
mssql_python.row.py: 77.6%
mssql_python.pybind.connection.connection_pool.cpp: 81.4%
mssql_python.__init__.py: 83.1%
mssql_python.pybind.connection.connection.cpp: 84.3%
mssql_python.logging.py: 85.5%
mssql_python.helpers.py: 89.3%

🔗 Quick Links

⚙️ Build Summary 📋 Coverage Details

View Azure DevOps Build

Browse Full Coverage Report

…ype stubs

- Remove the C++ env-var fallback (banned getenv/DevSkim finding); Python is already the sole authoritative resolver and pushes the selection via set_odbc_provider(). PoolingManager.enable() now also resolves+pushes so an explicit pooling() call before any connect still honors the selection.
- Fix two GetOdbcLibsBaseDir log messages that hardcoded 'mssql_python_odbc' regardless of the selected provider.
- Widen the public odbc_provider setter type hint to Optional[str] to match ProviderManager.set_property().
- Add odbc_provider and get_odbc_provider_info() to mssql_python.pyi (PEP 561 stubs).
- Make the missing-provider test deterministic by patching import_module instead of relying on the package being absent.
@gargsaumya gargsaumya changed the title Add opt-in/opt-out ODBC provider selection (msodbcsql18 / mssql-odbc) FEAT: Add opt-in/opt-out ODBC provider selection (msodbcsql18 / mssql-odbc) Aug 27, 2026
@github-actions github-actions Bot added the pr-size: medium Moderate update size label Aug 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-size: medium Moderate update size

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants