FEAT: [mssql-python] Add opt-in mssql-python connection backend - #596
Conversation
Add mssql-python as an opt-in DB-API driver, selected per-connection via
OPTIONS = {"python_driver": "mssql_python"}. pyodbc remains the default
and is unchanged: when the option is absent the connection string, retry
logic, and error handling take exactly the same path as before.
When the option is set, the backend lazily imports mssql-python (so it is
not required unless used), routes the connection through it, and adjusts
for the bundled driver:
- The connection string omits the ODBC-only DRIVER / DSN / SERVERNAME /
MARS_Connection keywords that the bundled driver rejects, and uses the
SERVER=host,port form.
- unicode_results (a pyodbc-only connect keyword) is not passed.
- Transient and network error numbers are matched against the exception
message text, since mssql-python does not expose a SQLSTATE in args.
- self.Database shadows the module-level pyodbc reference per connection so
Django's error wrapping and the cursor paths use the driver that actually
connected.
The default pyodbc unit tests are unchanged and continue to pass. Part of
the mssql-django 2.0 opt-in mssql-python work.
AB#
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
There’s a confirmed retry-loop correctness bug plus misleading installation guidance and missing coverage for the new python_driver behavior.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds an opt-in mssql-python DB-API driver path to the SQL Server backend’s DatabaseWrapper so individual Django connections can select an alternative driver via OPTIONS['python_driver'], while keeping pyodbc as the default code path.
Changes:
- Added lazy
mssql_pythonimport and per-connection driver selection/shadowing viaself.Database. - Adjusted connection-string construction to omit ODBC-only keywords for the
mssql-pythonpath and useSERVER=host,port. - Updated connect/retry/error-handling and cursor error wrapping to use the per-connection driver module.
File summaries
| File | Description |
|---|---|
| mssql/base.py | Introduces opt-in mssql-python driver selection, connection-string branching, and per-connection DB-API routing. |
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Merge current dev while preserving explicit MARS handling. Require the released driver, isolate connection selection, and fix retry state. Use native datetimeoffset conversion and add backend regression coverage. AB#48000 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Three moderate issues in mssql/base.py affect connection-parameter precedence and MARS capability handling.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (1)
mssql/base.py:515
- The opt-in path now emits
SERVERunconditionally, butextra_paramsis appended verbatim below this method. An explicitextra_params='SERVER=...'therefore produces duplicate server keywords, so the user's connection target may be ignored or rejected by the mssql-python parser. Treat an explicitly suppliedserverkey as authoritative before adding the default.
if use_python_driver:
# mssql-python bundles its own SQL Server driver and rejects the
# ODBC-only DRIVER / DSN / SERVERNAME keywords, so none are emitted.
if port:
host = ','.join((host, str(port)))
cstr_parts['SERVER'] = host
- Files reviewed: 3/3 changed files
- Comments generated: 2
- Review effort level: Lite
AB#48000 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Critical driver error-handling and UUID conversion issues remain, along with moderate alias and test-ordering issues.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (2)
mssql/base.py:562
- This exact-key filter does not handle mssql-python's connection-string aliases. For example,
Address=overrideis normalized by that driver to the same canonicalServerkey as the generatedSERVER, but the generated value appears first and wins, so the documented "explicit extras take precedence" behavior silently connects to the wrong host. Canonicalize aliases before removing generated keys (and cover the equivalent authentication/database aliases).
extra_params = self._parse_extra_params(options_extra_params)
cstr_parts = {
key: value for key, value in cstr_parts.items()
if key.lower() not in extra_params
}
testapp/tests/test_mssql_python_backend.py:248
- The query has no outer
ORDER BY, but the assertions require rows to arrive as 1..2000. SQL Server does not guarantee result order without an outer ordering clause, so this live regression test can fail when the execution plan changes or runs in parallel. Add an alias for the row number andORDER BYthat alias before asserting the sequence.
SELECT TOP (2000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)),
CAST(REPLICATE(N'x', 1024) AS nvarchar(1024))
FROM d a CROSS JOIN d b CROSS JOIN d c CROSS JOIN d e
""")
- Files reviewed: 3/3 changed files
- Comments generated: 2
- Review effort level: Lite
📊 Code Coverage Report
Diff CoverageDiff: dev...HEAD, staged and unstaged changesNo lines with coverage information in this diff. 📋 Files Needing Attention📉 Files with overall lowest coverage (click to expand)- mssql/__init__.py: 50.0% (2 lines)
- mssql/creation.py: 58.1% (74 lines)
- mssql/management/commands/inspectdb.py: 75.0% (12 lines)
- mssql/operations.py: 78.8% (396 lines)
- mssql/compiler.py: 83.4% (644 lines)
- mssql/client.py: 85.4% (41 lines)
- mssql/functions.py: 86.9% (428 lines)
- mssql/schema.py: 88.3% (719 lines)
- mssql/introspection.py: 90.2% (133 lines)
- mssql/base.py: 91.7% (554 lines)🔗 Quick Links
|
AB#48000 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🔵 Needs a closer look
Two moderate findings remain unresolved, and the backend changes require final human review.
Review details
Suppressed comments (2)
testapp/tests/test_base.py:767
connection.copy()only shallow-copies the settings dictionary, so mutating this nestedOPTIONSmapping also changes the default alias. In a mssql-python test run this forces the shared default connection back to pyodbc, making later tests/aliases use the wrong driver; copyOPTIONSbefore setting the per-alias override.
options["python_driver"] = "pyodbc"
testapp/tests/test_mssql_python_backend.py:274
- The copied wrapper shares the original alias's nested
OPTIONS, so this mutation leaksMARS_Connection=nointo the default connection settings. That makes subsequent tests reconnect with a different connection string and can invalidate MARS/driver-isolation coverage; clone theOPTIONSmapping before changingextra_params.
options["extra_params"] = "MARS_Connection=no;" + (options.get("extra_params") or "")
- Files reviewed: 3/3 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
The version check incorrectly accepts prerelease builds below the required 1.15.0 release.
Get a fresh assessment by requesting another Copilot review.
Review details
- Files reviewed: 3/3 changed files
- Comments generated: 1
- Review effort level: Balanced
Summary
Add per-connection mssql-python selection through OPTIONS.python_driver while preserving pyodbc as the default.
Validation
On Python 3.14 / Django 6.1.1 / SQL Server 2025, the owned suite completed with both pyodbc and published mssql-python 1.15.0: 250 tests per run, with 3 existing skips and 3 expected failures.
Live mixed-driver checks covered Decimal and binary values, UTC datetime round-trips, non-UTC datetimeoffset retrieval, commit/savepoint rollback, error translation, and reconnects. Cloud Entra authentication and the full Windows/Linux upstream-Django matrix remain release-integration gates; they were not validated here.
Dependencies
Additional tests, documentation, and packaging remain in #597, #598, and #599. No dependency on the superseded #595.
User Story: AB#47999
Task: AB#48000