Debian/Ubuntu
-
-Make sure to install the ODBC headers as well as the driver linked above:
+Install the ODBC headers as well as the driver linked above:
```shell
sudo apt-get install -y unixodbc-dev
```
-
-Latest version: 
+### `mssql-python` backend
+
+An alternative backend that does not require the ODBC driver.
```shell
-pip install -U dbt-sqlserver
+pip install -U "dbt-sqlserver[mssql]"
```
-Latest pre-release: 
+On Debian/Ubuntu-based systems, `mssql-python` requires these system libraries:
```shell
-pip install -U --pre dbt-sqlserver
+sudo apt-get install -y libltdl7 libkrb5-3 libgssapi-krb5-2
+```
+
+Enable it per target in your `profiles.yml`:
+
+```yaml
+your_profile:
+ target: dev
+ outputs:
+ dev:
+ type: sqlserver
+ host: your-server
+ port: 1433
+ database: your-database
+ schema: dbo
+ user: your-user
+ password: your-password
+ encrypt: true
+ trust_cert: false
+ backend: mssql-python # <-- enables this backend
```
## Changelog
@@ -51,8 +89,6 @@ See [the changelog](CHANGELOG.md)
## Configuration
-### Flags
-
- `dbt_sqlserver_use_default_schema_concat`: *(default: `false`)* Controls schema name generation when a [custom schema](https://docs.getdbt.com/docs/build/custom-schemas) is set on a model.
| Flag value | `custom_schema_name` | Result |
@@ -77,6 +113,31 @@ See [the changelog](CHANGELOG.md)
> **Note:** If you want to permanently customise schema generation and avoid any future changes, override the `sqlserver__generate_schema_name` macro directly in your project instead.
+### `dbt_sqlserver_use_default_schema_concat`
+
+*(default: `false`)* Controls schema name generation when a [custom schema](https://docs.getdbt.com/docs/build/custom-schemas) is set on a model.
+
+| Value | `custom_schema_name` | Result |
+|---|---|---|
+| `false` (default) | *(none)* | `target.schema` |
+| `false` (default) | `"reporting"` | `reporting` |
+| `true` | *(none)* | `target.schema` |
+| `true` | `"reporting"` | `target.schema_reporting` |
+
+When `false`, `custom_schema_name` is used as-is without being prefixed by `target.schema`.
+When `true`, the adapter delegates to dbt-core's `default__generate_schema_name`.
+
+```yaml
+# dbt_project.yml
+vars:
+ dbt_sqlserver_use_default_schema_concat: true
+```
+
+> **Note:** To permanently customise schema generation without a flag dependency, override the `sqlserver__generate_schema_name` macro directly in your project.
+
+### `backend`
+
+*(default: `pyodbc`)* Set to `mssql-python` in a profile target to use the `mssql-python` backend instead of `pyodbc`. The adapter fails if the required backend package (Python dependency), such as `pyodbc` or `mssql-python`, is not installed.
## Contributing
@@ -85,7 +146,7 @@ See [the changelog](CHANGELOG.md)
[](https://github.com/dbt-msft/dbt-sqlserver/actions/workflows/integration-tests-azure.yml)
This adapter is community-maintained.
-You are welcome to contribute by creating issues, opening or reviewing pull requests or helping other users in Slack channel.
+You are welcome to contribute by creating issues, opening or reviewing pull requests, or helping other users in the Slack channel.
If you're unsure how to get started, check out our [contributing guide](CONTRIBUTING.md).
## License
diff --git a/dbt/adapters/sqlserver/sqlserver_auth.py b/dbt/adapters/sqlserver/sqlserver_auth.py
new file mode 100644
index 000000000..3a7993fb9
--- /dev/null
+++ b/dbt/adapters/sqlserver/sqlserver_auth.py
@@ -0,0 +1,323 @@
+"""Authentication and token helpers for the SQL Server adapter.
+
+This module owns the shared normalization rules for auth labels, plus the
+pyodbc-facing Azure token helpers used by the connection manager.
+"""
+
+from __future__ import annotations
+
+import struct
+import time
+from itertools import chain, repeat
+from typing import TYPE_CHECKING, Any, Callable, Dict, Mapping, Optional, cast
+
+from dbt.adapters.events.logging import AdapterLogger
+from dbt.adapters.sqlserver.sqlserver_constants import (
+ AAD_TOKEN_AUTHENTICATIONS,
+ CONNECTION_AUTH_ALIASES,
+ CONNECTION_AUTH_PASSTHROUGH_KEYS,
+ PYODBC_AUTH_ALIASES,
+ SQLSERVER_BACKEND_MSSQL_PYTHON,
+)
+from dbt.adapters.sqlserver.sqlserver_runtime import (
+ AZURE_CREDENTIAL_SCOPE,
+ AccessTokenProtocol,
+ _get_azure_access_token_class,
+ _get_azure_identity_module,
+ _get_cached_access_token,
+)
+
+if TYPE_CHECKING:
+ from dbt.adapters.sqlserver.sqlserver_credentials import SQLServerBackend, SQLServerCredentials
+
+
+logger = AdapterLogger("sqlserver")
+AZURE_AUTH_FUNCTION_TYPE = Callable[[Any, Optional[str]], AccessTokenProtocol]
+
+
+def is_mssql_python_backend(backend: "SQLServerBackend") -> bool:
+ """Return whether the coerced backend enum targets ``mssql-python``."""
+
+ return backend.value == SQLSERVER_BACKEND_MSSQL_PYTHON
+
+
+def normalize_authentication_key(value: Optional[str]) -> str:
+ """Normalize a SQL Server auth or lookup key for cross-layer comparisons."""
+
+ return "" if value is None else value.replace("_", "").replace(" ", "").lower()
+
+
+def is_active_directory_authentication(authentication: Optional[str]) -> bool:
+ """Return whether an auth label targets one of the ActiveDirectory modes."""
+
+ return normalize_authentication_key(authentication).startswith("activedirectory")
+
+
+def normalize_mssql_python_authentication(
+ authentication: Optional[str],
+) -> Optional[str]:
+ """Backend-layer auth normalization used while building connection strings."""
+
+ authentication = authentication or ""
+ key = normalize_authentication_key(authentication)
+ if not key:
+ return None
+
+ if key in CONNECTION_AUTH_PASSTHROUGH_KEYS:
+ return authentication.strip()
+
+ if key in CONNECTION_AUTH_ALIASES:
+ return CONNECTION_AUTH_ALIASES[key]
+
+ return authentication.strip()
+
+
+def normalize_pyodbc_authentication(authentication: Optional[str]) -> str:
+ """Normalize auth labels for the pyodbc token path.
+
+ Only the token-oriented aliases that participate in cached access-token
+ retrieval are normalized here. Connection-string auth aliases such as
+ ``ActiveDirectoryServicePrincipal`` are handled by the backend builders.
+ """
+
+ if key := normalize_authentication_key(authentication):
+ return PYODBC_AUTH_ALIASES.get(key, key)
+ return ""
+
+
+def normalize_connection_authentication(
+ authentication: Optional[str], mssql_python_backend: bool
+) -> str:
+ """Normalize auth labels for connection-string generation.
+
+ Call this from connection-string builders and validation, not from profile
+ parsing. The ``mssql-python`` path canonicalizes long-form connection
+ strings, while the pyodbc path preserves its raw token-auth labels so
+ ``get_pyodbc_attrs_before_credentials`` can apply its narrower alias map.
+ """
+
+ authentication = authentication or ""
+ if mssql_python_backend:
+ return normalize_mssql_python_authentication(authentication) or ""
+ return authentication.strip()
+
+
+def uses_aad_token_authentication(credentials: "SQLServerCredentials") -> bool:
+ """Return whether pyodbc should request and cache an Azure access token.
+
+ This is used by retry policy as well as token fetching, so manual
+ ``ActiveDirectoryAccessToken`` profiles stay in the same retry bucket as
+ the other AAD token modes.
+ """
+
+ authentication = normalize_pyodbc_authentication(credentials.authentication)
+ return authentication in AAD_TOKEN_AUTHENTICATIONS
+
+
+def get_environment_access_token(
+ credentials: SQLServerCredentials, scope: Optional[str] = AZURE_CREDENTIAL_SCOPE
+) -> AccessTokenProtocol:
+ """
+ Get an Azure access token by reading environment variables
+
+ Parameters
+ -----------
+ credentials: SQLServerCredentials
+ Credentials.
+
+ Returns
+ -------
+ out : AccessToken
+ The access token.
+ """
+ azure_identity = _get_azure_identity_module()
+ return azure_identity.EnvironmentCredential().get_token(
+ scope, timeout=credentials.login_timeout
+ )
+
+
+def get_msi_access_token(
+ _credentials: SQLServerCredentials, scope: Optional[str] = AZURE_CREDENTIAL_SCOPE
+) -> AccessTokenProtocol:
+ """
+ Get an Azure access token from the system's managed identity
+
+ Parameters
+ -----------
+ credentials: SQLServerCredentials
+ Credentials.
+
+ Returns
+ -------
+ out : AccessToken
+ The access token.
+ """
+ azure_identity = _get_azure_identity_module()
+ return azure_identity.ManagedIdentityCredential().get_token(scope or AZURE_CREDENTIAL_SCOPE)
+
+
+def convert_bytes_to_mswindows_byte_string(value: bytes) -> bytes:
+ """
+ Convert bytes to a Microsoft windows byte string.
+
+ Parameters
+ ----------
+ value : bytes
+ The bytes.
+
+ Returns
+ -------
+ out : bytes
+ The Microsoft byte string.
+ """
+ encoded_bytes = bytes(chain.from_iterable(zip(value, repeat(0))))
+ return struct.pack("