Skip to content

REFACTOR: remove bundled ODBC driver from mssql-python wheel; require mssql-python-odbc (Phase 2) - #693

Open
Jahnvi Thakkar (jahnvi480) wants to merge 22 commits into
mainfrom
jahnvi/phase2-remove-bundled-libs
Open

REFACTOR: remove bundled ODBC driver from mssql-python wheel; require mssql-python-odbc (Phase 2)#693
Jahnvi Thakkar (jahnvi480) wants to merge 22 commits into
mainfrom
jahnvi/phase2-remove-bundled-libs

Conversation

@jahnvi480

@jahnvi480 Jahnvi Thakkar (jahnvi480) commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Work Item / Issue Reference

AB#46836


Summary

This pull request makes significant updates to the build and packaging process for the mssql-python and mssql-python-odbc Python packages, reflecting the transition to Phase 2 where the ODBC driver is fully separated into its own package. The pipeline now always builds both packages together, ensures the external ODBC wheel is installed before testing, and simplifies and streamlines build and test stages. Documentation and prompts are also updated to reflect these structural changes.

Build and Pipeline Simplification:

  • The pipeline now always builds both mssql-python and mssql-python-odbc packages together; parameters and logic for building only one package have been removed, simplifying configuration and execution. [1] [2]
  • The ODBC build is consolidated into a single ODBC_BuildAll stage that produces all platform wheels from one host, replacing multiple per-platform ODBC build stages.
  • All mssql-python build stages now depend on the ConsolidateOdbc stage and install the external ODBC wheel before running tests, validating the package split end-to-end during the build. The previous post-consolidation "TestBothWheels" stages are removed as redundant.

Documentation and Prompt Updates:

  • Documentation and prompts are updated to reference the new mssql_python_odbc/libs location for ODBC binaries and to clarify that these are now maintained in the standalone mssql-python-odbc package.

Platform and Security Improvements:

  • The macOS build pool is updated to use the latest available image (macos-latest, macOS-15 Sequoia).
  • The SBOM (Software Bill of Materials) is now always named after the primary mssql-python wheel, reflecting the unified build process.

Release Pipeline Robustness:

  • The release pipeline now derives the expected ODBC package version directly from mssql_python_odbc/__init__.py, ensuring the version pin is always in sync and eliminating the need for manual updates when the ODBC version changes.

…ython-odbc

Phase 2 of the ODBC package split. Move the driver binaries from mssql_python/libs to mssql_python_odbc/libs so only the mssql-python-odbc distribution ships them. Remove the C++ bundled-libs fallback in GetOdbcLibsBaseDir(): a missing or incomplete mssql_python_odbc now raises a clear error instead of silently using bundled libs. Drop libs packaging from setup.py, retire sync_libs() in setup_odbc.py, and repoint build.bat vcredist source. AB#46586
Piece A: setup_odbc.py builds all 7 py3-none-<platform> ODBC data wheels from one host via ODBC_TARGET_* overrides; per-target package_data (include_package_data=False) so each wheel ships only its own driver + LICENSING; plat_name_supplied=True fixes macOS wheel tagging. Piece B: one unconditional ODBC_BuildAll + ConsolidateOdbc stage runs first; Windows/macOS/Linux mssql-python build stages dependsOn ConsolidateOdbc and install the external mssql-python-odbc wheel before pytest (Linux via PIP_FIND_LINKS into the bind-mounted repo dir).
The mssql-python build stages now install the external mssql-python-odbc wheel and run the full pytest suite, so the separate post-consolidation wheel-installation test stages are redundant.
…cOS-14 agent

The pure-data ODBC wheel was tagged macosx_15_0_universal2, which pip rejects on the macOS-14 build/test agent (from versions: none). Lower it to the canonical universal2 floor (macosx_11_0); the driver dylibs run on macOS 11+ and the tag stays compatible everywhere the mssql-python wheel installs.
Phase 2 moved the bundled ODBC driver libs from mssql_python into the external mssql_python_odbc package. test_libs_directory_exists and test_auth_dll_exists_if_libs_present now resolve the libs/ base dir via a helper that prefers mssql_python_odbc (falling back to mssql_python for the pre-split layout).
Copilot AI review requested due to automatic review settings July 31, 2026 08:35
@github-actions github-actions Bot added the pr-size: large Substantial code update label Jul 31, 2026
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown

📊 Code Coverage Report

🔥 Diff Coverage

60%


🎯 Overall Coverage

82%


📈 Total Lines Covered: 7368 out of 8963
📁 Project: mssql-python


Diff Coverage

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

  • mssql_python/pybind/ddbc_bindings.cpp (60.0%): Missing lines 1164,1178-1180,1476,1478,1486-1487

Summary

  • Total: 20 lines
  • Missing: 8 lines
  • Coverage: 60%

mssql_python/pybind/ddbc_bindings.cpp

Lines 1160-1168

  1160         if (!externalComplete) {
  1161             LOG("GetOdbcLibsBaseDir: mssql_python_odbc present at '%s' but its ODBC driver "
  1162                 "binaries are missing or incomplete for this platform",
  1163                 parentDir.string().c_str());
! 1164             ThrowStdException(
  1165                 "The 'mssql-python-odbc' package is installed but its ODBC driver binaries "
  1166                 "are missing or incomplete for this platform. Reinstall it with: "
  1167                 "pip install --force-reinstall mssql-python-odbc");
  1168         }

Lines 1174-1184

  1174             // Phase 2: the standalone package is required. Turn the missing
  1175             // dependency into a clear, actionable error instead of a fallback.
  1176             LOG("GetOdbcLibsBaseDir: required package mssql_python_odbc is not installed (%s)",
  1177                 e.what());
! 1178             ThrowStdException(
! 1179                 "The required 'mssql-python-odbc' package (which ships the ODBC driver "
! 1180                 "binaries) is not installed. Install it with: pip install mssql-python-odbc");
  1181         }
  1182         // A different import-time error means the package is installed but
  1183         // broken; surface it instead of silently masking the real problem.
  1184         LOG("GetOdbcLibsBaseDir: importing mssql_python_odbc failed unexpectedly (%s); "

Lines 1472-1482

  1472     // the actionable "install mssql-python-odbc" error into a hard crash. So we
  1473     // capture any failure as an exception_ptr inside the callable and rethrow it
  1474     // below, in a normal context that pybind11 can translate into a Python
  1475     // exception. The stored error persists, so every subsequent call re-raises the
! 1476     // same actionable message instead of silently proceeding without a driver.
  1477     std::call_once(m_onceFlag, [this]() {
! 1478         try {
  1479             LoadDriverOrThrowException();
  1480             m_driverLoaded = true;
  1481         } catch (...) {
  1482             m_loadError = std::current_exception();

Lines 1482-1491

  1482             m_loadError = std::current_exception();
  1483         }
  1484     });
  1485     if (m_loadError) {
! 1486         std::rethrow_exception(m_loadError);
! 1487     }
  1488 }
  1489 
  1490 // SqlHandle definition
  1491 SqlHandle::SqlHandle(SQLSMALLINT type, SQLHANDLE rawHandle) : _type(type), _handle(rawHandle) {}


📋 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: 59.9%
mssql_python.pybind.logger_bridge.hpp: 70.8%
mssql_python.pybind.ddbc_bindings.cpp: 76.6%
mssql_python.__init__.py: 77.6%
mssql_python.row.py: 77.6%
mssql_python.ddbc_bindings.py: 79.6%
mssql_python.pybind.connection.connection_pool.cpp: 81.4%
mssql_python.pybind.connection.connection.cpp: 84.3%
mssql_python.logging.py: 85.5%

🔗 Quick Links

⚙️ Build Summary 📋 Coverage Details

View Azure DevOps Build

Browse Full Coverage Report

Copilot stopped reviewing on behalf of Jahnvi Thakkar (jahnvi480) due to an error July 31, 2026 08:56

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

Note

Copilot was unable to run its full agentic suite in this review.

This PR finalizes the Phase 2 split of the bundled ODBC driver into the standalone mssql-python-odbc package, makes the external package required at runtime, and updates build/test pipelines to produce and consume the new wheel layout.

Changes:

  • Make mssql_python_odbc required for driver resolution (no fallback to bundled mssql_python/libs), with clearer runtime errors.
  • Update ODBC wheel packaging to include only the target platform’s libs/ subtree and enable single-host cross-building of all ODBC wheels.
  • Refactor CI pipelines to build all ODBC wheels in one Windows stage, install the ODBC wheel before pytest in platform builds, and remove now-redundant “both wheels” installation test stages.

Reviewed changes

Copilot reviewed 17 out of 85 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/test_015_utf8_path_handling.py Updates tests to locate driver libs in mssql_python_odbc (but still includes a fallback).
setup_odbc.py Packages only the target platform’s ODBC libs/ subtree; adds env overrides for cross-building and forces wheel to trust supplied platform tags.
setup.py Removes inclusion/exclusion rules for bundled libs/ since the driver is now shipped separately.
mssql_python/pybind/ddbc_bindings.cpp Requires mssql_python_odbc and throws actionable errors if missing/incomplete; removes fallback behavior.
mssql_python/pybind/build.bat Copies VC++ runtime from mssql_python_odbc/libs/.../vcredist instead of mssql_python/libs.
eng/pipelines/pr-validation-pipeline.yml Updates validation paths to the new mssql_python_odbc/libs/... locations.
OneBranchPipelines/stages/wheel-installation-test-windows-stage.yml Deleted: prior end-user “both wheels” installation test stage (Windows).
OneBranchPipelines/stages/wheel-installation-test-stage.yml Deleted: prior end-user “both wheels” installation test stage (Linux).
OneBranchPipelines/stages/wheel-installation-test-macos-stage.yml Deleted: prior end-user “both wheels” installation test stage (macOS).
OneBranchPipelines/stages/build-windows-single-stage.yml Adds optional dependency/install of consolidated mssql-python-odbc wheel before pytest.
OneBranchPipelines/stages/build-odbc-windows-stage.yml Deleted: replaced by a single-host “build all ODBC wheels” stage.
OneBranchPipelines/stages/build-odbc-macos-stage.yml Deleted: replaced by a single-host “build all ODBC wheels” stage.
OneBranchPipelines/stages/build-odbc-linux-stage.yml Deleted: replaced by a single-host “build all ODBC wheels” stage.
OneBranchPipelines/stages/build-odbc-all-stage.yml New: builds all 7 ODBC wheels on one Windows agent via ODBC_TARGET_* overrides and validates wheel contents.
OneBranchPipelines/stages/build-macos-single-stage.yml Adds optional dependency/install of consolidated mssql-python-odbc wheel before pytest.
OneBranchPipelines/stages/build-linux-single-stage.yml Adds optional dependency/install of consolidated mssql-python-odbc wheel before pytest (via PIP_FIND_LINKS).
OneBranchPipelines/build-release-package-pipeline.yml Always builds both packages; makes mssql-python stages depend on ConsolidateOdbc; swaps 7 ODBC stages for one build-all stage.
.gitignore Stops ignoring mssql_python_odbc/libs/ now that it’s the committed source of truth.
Suppressed comments (2)

tests/test_015_utf8_path_handling.py:1

  • This test helper still falls back to mssql_python when mssql_python_odbc is missing/incomplete, but the runtime resolver (GetOdbcLibsBaseDir) now requires the external package and throws if it’s missing or incomplete. The fallback makes the tests inconsistent with production behavior and can mask packaging issues. Recommendation (mandatory): remove the fallback and instead fail (or explicitly pytest.skip) when mssql_python_odbc is not importable or does not contain a complete libs/ payload for the platform.
    setup_odbc.py:1
  • When ODBC_TARGET_PLATFORM_TAG is set, get_platform_info() returns an empty arch if ODBC_TARGET_ARCH is unset. That empty value is later used to build the target libs/ globs (e.g., libs/windows/{arch}), which can silently produce wrong/empty package_data and yield broken wheels. Recommendation (mandatory): validate the override inputs—require a non-empty, expected arch for win* and *linux* tags (and optionally validate allowed values per tag), and raise a clear error when the override is incomplete/invalid.

Comment thread OneBranchPipelines/stages/build-odbc-all-stage.yml
…); validate installed mssql-python-odbc wheel on Win/macOS pytest; repoint configure_dylibs to mssql_python_odbc/libs
…exact driver filenames

- setup_odbc.get_platform_info now raises OSError if ODBC_TARGET_PLATFORM_TAG is set but ODBC_TARGET_ARCH is empty; an empty arch expanded the libs/ package_data globs to every architecture and could leak foreign-platform driver binaries into the wheel.

- build-odbc-all-stage leak-check Must lists now assert the exact driver filenames (msodbcsql18.dll / libmsodbcsql.18.dylib / libmsodbcsql-18) instead of loose substrings that also matched support files.

- test_015 prose updated to reference GetOdbcLibsBaseDir (the successor to the removed GetModuleDirectory).
…bundled-libs

# Conflicts:
#	OneBranchPipelines/stages/build-odbc-linux-stage.yml
#	OneBranchPipelines/stages/wheel-installation-test-stage.yml

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

need to add some failure case tests, and some improvement suggestions

Comment thread setup.py Outdated
Comment thread mssql_python/pybind/ddbc_bindings.cpp
Comment thread OneBranchPipelines/stages/build-odbc-all-stage.yml Outdated
Jahnvi Thakkar (jahnvi480) added a commit that referenced this pull request Aug 5, 2026
… description (#701)

### Work Item / Issue Reference  
<!-- 
IMPORTANT: Please follow the PR template guidelines below.
For mssql-python maintainers: Insert your ADO Work Item ID below 
For external contributors: Insert Github Issue number below
Only one reference is required - either GitHub issue OR ADO Work Item.
-->

<!-- mssql-python maintainers: ADO Work Item -->
>
[AB#46836](https://sqlclientdrivers.visualstudio.com/c6d89619-62de-46a0-8b46-70b92a84d85e/_workitems/edit/46836)

-------------------------------------------------------------------

### Summary

Metadata-only re-release of the standalone **mssql-python-odbc** package
(identical driver binaries as the already-published 18.6.2) that
corrects the package license and ships a proper PyPI description.

## What changed

- **License: MIT → Other/Proprietary License** — the `license` field and
trove classifier in `setup_odbc.py`. The wheel redistributes Microsoft's
proprietary ODBC Driver 18 for SQL Server (and the VC++ runtime it links
against), so the previous `MIT` label was incorrect. The build script's
own source stays MIT.
- **PyPI `long_description`** — replaced the plain one-line description
with a rendered Markdown description: project role, "do not install
directly" guidance, docs link, and a **License information** section
linking both redistributed Microsoft license texts (same style as
`azure-cognitiveservices-speech`). `long_description_content_type`
switched to `text/markdown`.
- **Committed canonical `mssql_python_odbc/licenses/` dir** — byte-exact
copies of the ODBC Driver and Visual Studio license texts. The
description links and `license_files` now point here, so the links
resolve on PyPI/GitHub and the wheel embeds the license texts
deterministically — independent of the build-time-populated `libs/` tree
(empty on `main`).
- **Version 18.6.2 → 18.6.2.1** — packaging revision 1 of the same
driver. A literal `v1` suffix is not a valid PEP 440 version and
`+local` versions are rejected by PyPI's upload API, so the revision is
expressed as a fourth numeric component. Ordering holds: `18.6.2 <
18.6.2.1 < 18.6.3`.
- **Relaxed `mssql_python/pybind/CMakeLists.txt` version parser** — now
accepts a trailing packaging-revision component / PEP 440 suffix. Only
the leading MAJOR and MAJOR.MINOR are used to derive the driver
filename, so the extra component has no effect on which driver is
loaded.

## Ordering

This lands **before** the Phase 2 wheel-slimming PR (#693). After this
release publishes `18.6.2.1` to PyPI, a small follow-up bumps
`mssql-python`'s pinned dependency from `==18.6.2` to `==18.6.2.1` so
installs pull the correctly-licensed wheel.

## Validation

- `black --check --line-length=100 setup_odbc.py` — passes.
- CMake version-parse of `18.6.2.1` exercised with the real regex engine
→ `MAJOR=18`, `MAJOR_MINOR=18.6`, no `FATAL_ERROR`.
- Driver binaries unchanged from 18.6.2 (metadata-only re-release).
Comment thread tests/test_025_odbc_package_required.py Dismissed
…macosx_15_0

The mssql-python wheel is tagged macosx_15_0_universal2 while the odbc wheel was tagged macosx_11_0_universal2, so the release pipeline never install-tested the 15_0 floor and the two wheels disagreed on the macOS floor. Bump the OneBranch macOS build/test agent to macos-latest (macOS-15, matching the pr-validation gate) and tag the odbc universal2 wheel macosx_15_0 (matrix + content-verification map) so both wheels share one floor and 15_0 is install-tested end to end.
…opy is removed

The multi-platform wheel-build stage deletes the committed mssql_python_odbc/ directory before running setup.py bdist_wheel (so pytest resolves the driver from the installed wheel, not the checkout). _read_odbc_version() read that now-deleted __init__.py and crashed the Windows x64 and macOS wheel builds with FileNotFoundError. Read the checkout copy when present (local dev, Windows arm64 where the dir is not removed), otherwise fall back to the already-installed mssql-python-odbc package metadata -- both derive from the same __version__, so it stays a single source of truth.
On musl libc (Alpine/musllinux) libstdc++ routes std::call_once through pthread_once, which cannot propagate an exception thrown by the callable and instead calls std::terminate() (SIGABRT). That turned the actionable 'install mssql-python-odbc' driver-load error into a hard crash on Alpine (test_025 aborted with returncode -6).

Capture any failure from LoadDriverOrThrowException() as a std::exception_ptr inside the call_once callable and rethrow it from loadDriver() in a normal context, where pybind11 translates it into a Python exception. The stored error persists so every subsequent call re-raises the same actionable message.
The manylinux_2_28 aarch64 wheel build failed with 'sql.h: No such file or directory'. On Linux the ODBC Driver Manager headers come from the system unixODBC dev package in /usr/include, installed inside the build container via dnf/apk. On QEMU-emulated aarch64 that in-container install is unreliable (rpm/dnf can crash under emulation), leaving /usr/include/sql.h absent -- which only surfaced later as an opaque compile error.

The ODBC headers are architecture- and libc-agnostic (pure API declarations), so install unixodbc-dev natively on the x86_64 host and docker cp the headers into the build container. This makes header provisioning deterministic regardless of container arch/libc. Also add a gate that fails loudly with the real cause if sql.h is still missing, instead of the downstream compile error.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-size: large Substantial code update

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants