Skip to content

Feat/mssql python Add optional mssql-python backend, make pyodbc optional, and update docs / CI#681

Open
axellpadilla wants to merge 5 commits into
dbt-msft:masterfrom
axellpadilla:feat/mssql-python
Open

Feat/mssql python Add optional mssql-python backend, make pyodbc optional, and update docs / CI#681
axellpadilla wants to merge 5 commits into
dbt-msft:masterfrom
axellpadilla:feat/mssql-python

Conversation

@axellpadilla
Copy link
Copy Markdown
Collaborator

This PR introduces the new optional mssql-python connection backend while keeping the legacy pyodbc ODBC path available as an explicit extra.

Key changes:

  • Add lazy-loaded backend support in sqlserver_connections.py
  • Make both pyodbc and mssql-python optional extras in pyproject.toml
  • Update sqlserver_credentials.py for backend selection
  • Add/adjust unit tests for lazy import and backend behavior
  • Update integration test matrix in integration-tests-sqlserver.yml to preserve coverage without increasing total run count
  • Add devcontainer support for both backend extras in setup_env.sh
  • Clarify installation and usage in README.md
  • Add developer setup notes and dependency guidance to CONTRIBUTING.md
  • Small support changes in test.env.sample, conftest.py, and uv.lock

Why this change:

  • enables the modern mssql-python backend behind currently gated use_mssql_python: true
  • Avoids requiring system dependencies unless a backend is explicitly selected
  • keeps existing pyodbc behavior intact for legacy profiles
  • improves documentation and local development setup

Testing:

  • updated unit tests for both backend import behavior
  • CI workflow preserves the same total run count by partitioning backends across Python versions
  • devcontainer setup installs both pyodbc and mssql-python extras for local validation

@axellpadilla axellpadilla added this to the v1.10.0 milestone May 21, 2026
@Benjamin-Knight
Copy link
Copy Markdown
Collaborator

Ok, its a big change so will take a while to review, I can see a few issues I think so I'll find some time at start providing feedback.

My main concern other than the actual code change is the install method for the adapter now changes as we have to specify the driver we want to use, that is a big change. This doesn't just affect us, DBT themselves will have the wrong install instructions.

https://docs.getdbt.com/docs/local/connect-data-platform/mssql-setup?version=1.12#prerequisites

Copy link
Copy Markdown
Collaborator

@Benjamin-Knight Benjamin-Knight left a comment

Choose a reason for hiding this comment

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

Probably need some new tests as well but will look at those after these comments.

"decimal.Decimal": "decimal",
}

MSSQL_PYTHON_UNSUPPORTED_AUTHENTICATIONS = {
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.

You do not have MSI in this list, there is no MSI code in the new adapter connection handling, its only in get_pyodbc_attrs_before_credentials.

getattr(mssql_python, "OperationalError", Exception),
]

if _requires_pyodbc_backend(credentials):
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.

This is inside a mssql python branch but states its pyodbc, I think the _requires_pyodbc_backend just needs renaming, I think this might have been an attempt to standardize logic between two branches but the same logic appears without the function on line 573 as well? Probably needs calling there as well?

con_str = [f"SERVER={_build_server_arg(credentials)}"]
con_str.append(f"Database={credentials.database}")

assert credentials.authentication is not None
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.

These assert statements, so here and L494-495 are not safe, if run in optimization mode they are dropped and these values could end up resolving to false or something unexpected, just raise a DBT exception of some kind.

database: str
schema: str
driver: Optional[str] = None
host: str = ""
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.

These string defaults do not look safe, previously not provided a value would cause a proper error before connection, now it will just try and use invalid blank strings.

Comment thread pyproject.toml
"pyodbc>=5.2.0",
]
mssql = [
"mssql-python>=1.1.0",
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.

Why such an old version of the driver? We need at least 1.4.0 for the fixes to the datetime handling.

pyodbc.InternalError, # not used according to docs, but defined in PEP-249
pyodbc.OperationalError,
]
mssql_python.pooling(enabled=False)
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.

pyodbc uses pooling, why are we defaulting to no pooling on mssql_python?

backend:
- pyodbc
- mssql-python
exclude:
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.

pyodbc is still going to be the default for most users, at least to start, I do not think we should drop python version testing from pyodbc, I'm okay with the recent python versions only for the mssql-python, if you are changing driver you are likely on up to date python.

application_name = f"dbt-{credentials.type}/{plugin_version}"
con_str.append(f"APP={application_name}")

try:
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.

Don't think we need a try except around list appends?



def _validate_pyodbc_requirements(credentials: SQLServerCredentials) -> None:
if not credentials.driver:
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.

This would allow someone providing a driver value of "", we probably want something stronger like.
if credentials.driver is None or not credentials.driver.strip():

assert "PWD={super-secret}" in con_str
assert "encrypt=Yes" in con_str
assert "TrustServerCertificate=Yes" in con_str
assert "APP=dbt-sqlserver/" not in con_str
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.

Why are we testing that the APP value is no longer set? We set it in pyodbc and its useful when debugging server side, does the new driver not support it? Its a common connection string value? I can see we do not try and set it in the new driver connection code.

@axellpadilla
Copy link
Copy Markdown
Collaborator Author

@Benjamin-Knight I'm moving this to v1.11, I'm more interested in the other patches for v1.10 and its true we will need more time holding the rc1 pending until dbt docs are updated

@axellpadilla axellpadilla modified the milestones: v1.10.0, v1.11.0 May 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants