Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion .github/workflows/test_destinations_local.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ jobs:
# NOTE: we only run non-staging tests, as staging tests require credentials for s3 and azure
excluded_destination_configurations: "[\"clickhouse-parquet-staging-s3-authorization\", \"clickhouse-parquet-staging-az-authorization\", \"clickhouse-jsonl-staging-az-authorization\", \"clickhouse-jsonl-staging-s3-authorization\"]"

# MSSQL (local SQL Server in Docker, no cloud credentials required).
# The mssql-python driver bundles its client libraries, so no ODBC driver install is needed.
- display_name: mssql
destinations: "[\"mssql\"]"
filesystem_drivers: "[\"memory\"]"
extras: "--extra mssql --extra parquet"
needs_mssql: true

# Dremio
- display_name: dremio
destinations: "[\"dremio\"]"
Expand Down Expand Up @@ -167,6 +175,15 @@ jobs:
name: Start ClickHouse OSS
if: ${{ matrix.needs_clickhouse }}

- name: Start SQL Server and create database
if: ${{ matrix.needs_mssql }}
run: |
docker compose -f "tests/load/mssql/docker-compose.yml" up -d
echo "Waiting for SQL Server to accept connections..."
timeout 90s bash -c 'until docker exec dlt_test_mssql /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "Strong!Passw0rd" -C -Q "SELECT 1" >/dev/null 2>&1; do sleep 2; done'
docker exec dlt_test_mssql /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "Strong!Passw0rd" -C -Q "IF DB_ID('dlt_data') IS NULL CREATE DATABASE dlt_data"
echo "SQL Server is up and dlt_data is ready"

#
# Setup python and run tests
#
Expand Down Expand Up @@ -215,4 +232,8 @@ jobs:

- name: Stop ClickHouse OSS
if: always()
run: docker compose -f "tests/load/clickhouse/docker-compose.yml" down -v
run: docker compose -f "tests/load/clickhouse/docker-compose.yml" down -v

- name: Stop SQL Server
if: always()
run: docker compose -f "tests/load/mssql/docker-compose.yml" down -v
3 changes: 0 additions & 3 deletions .github/workflows/test_destinations_remote.yml
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,6 @@ jobs:
destinations: "[\"mssql\"]"
filesystem_drivers: "[\"memory\"]"
extras: "--extra mssql --extra s3 --extra gs --extra az --extra parquet --group adbc"
pre_install_commands: "sudo apt-get update && sudo ACCEPT_EULA=Y apt-get install --yes msodbcsql18"
post_install_commands: "uv run dbc install mssql"
always_run_all_tests: true

Expand All @@ -157,14 +156,12 @@ jobs:
destinations: "[\"synapse\"]"
filesystem_drivers: "[\"memory\"]"
extras: "--extra synapse --extra parquet"
pre_install_commands: "sudo apt-get update && sudo ACCEPT_EULA=Y apt-get install --yes msodbcsql18"

# Fabric
- display_name: fabric
destinations: "[\"fabric\"]"
filesystem_drivers: "[\"memory\"]"
extras: "--extra fabric"
pre_install_commands: "sudo apt-get update && sudo ACCEPT_EULA=Y apt-get install --yes msodbcsql18"
post_secrets_commands: "uv run python tests/load/fabric/refresh_spn_fabric_token.py"

# Postgres and Redshift (used to be test_destinations.yml)
Expand Down
90 changes: 55 additions & 35 deletions dlt/destinations/impl/fabric/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,30 @@
from dlt.common.configuration import configspec
from dlt.common.configuration.specs import AzureServicePrincipalCredentials
from dlt.common.destination.client import DestinationClientDwhWithStagingConfiguration
from dlt.common.exceptions import MissingDependencyException
from dlt.common.typing import TSecretStrValue
from dlt.common.utils import digest128
from dlt import version
from dlt.destinations.impl.mssql.configuration import (
apply_authentication_to_dsn,
build_token_attrs_before,
validate_authentication,
)

_AZURE_STORAGE_EXTRA = f"{version.DLT_PKG_NAME}[az]"
# Fabric Warehouse only supports Entra ID authentication, so it defaults to Service Principal.
_DEFAULT_AUTHENTICATION = "ActiveDirectoryServicePrincipal"


@configspec(init=False)
class FabricCredentials(AzureServicePrincipalCredentials):
"""Credentials for Microsoft Fabric Warehouse with Service Principal authentication.
"""Credentials for Microsoft Fabric Warehouse.

Fabric Warehouse requires Azure AD Service Principal authentication.
Inherits from AzureServicePrincipalCredentials for Service Principal fields and
automatic fallback to DefaultAzureCredential.
"""
Supports several Entra ID authentication methods, selected through `authentication`:
`ActiveDirectoryServicePrincipal` (default), `ActiveDirectoryPassword`,
`ActiveDirectoryIntegrated`, `ActiveDirectoryInteractive`, `ActiveDirectoryMsi`,
`ActiveDirectoryDefault` (alias `default`), `ActiveDirectoryDeviceCode`. All are passed
straight through as `Authentication=` in the DSN — mssql-python performs the sign-in.

drivername: str = "mssql+pyodbc"
"""SQLAlchemy driver name for SQL Server/Fabric."""
Inherits from AzureServicePrincipalCredentials for the Service Principal fields.
"""

host: str = None
"""Fabric Warehouse host (e.g., abc12345-6789-def0-1234-56789abcdef0.datawarehouse.fabric.microsoft.com)"""
Expand All @@ -35,45 +41,59 @@ class FabricCredentials(AzureServicePrincipalCredentials):
connect_timeout: int = 15
"""Connection timeout in seconds (default: 15)"""

authentication: str = _DEFAULT_AUTHENTICATION
"""Authentication method, passed straight through as `Authentication=` in the DSN:
`ActiveDirectoryServicePrincipal` (default), `ActiveDirectoryPassword`,
`ActiveDirectoryIntegrated`, `ActiveDirectoryInteractive`, `ActiveDirectoryMsi`,
`ActiveDirectoryDefault` (alias `default`), `ActiveDirectoryDeviceCode`."""

username: str | None = None
"""User principal name, used with `ActiveDirectoryPassword` authentication."""

password: TSecretStrValue | None = None
"""Password, used with `ActiveDirectoryPassword` authentication."""

# Override to make optional - not needed for Fabric Warehouse credentials (only for staging)
azure_storage_account_name: Optional[str] = None
"""Not used for Fabric Warehouse credentials (only staging credentials need this)"""

def on_partial(self) -> None:
"""Enable fallback to DefaultAzureCredential if explicit credentials not provided."""
try:
from azure.identity import DefaultAzureCredential
except ModuleNotFoundError:
raise MissingDependencyException(self.__class__.__name__, [_AZURE_STORAGE_EXTRA])

# If no explicit Service Principal credentials, use default credentials
if not self.azure_client_id or not self.azure_client_secret or not self.azure_tenant_id:
self._set_default_credentials(DefaultAzureCredential())
# Resolve if we have warehouse connection details (not storage account name)
if self.host and self.database:
self.resolve()
"""Resolve once host and database are known.

Fabric always has an `authentication` value set (default: Service Principal), so this
resolves as soon as the connection target is known. `on_resolved` validates. Auth logic
is shared with `MsSqlCredentials`.
"""
# Resolve if we have the warehouse connection details (not the storage account name)
if self.host and self.database:
self.resolve()

def on_resolved(self) -> None:
"""Validate the configured authentication method."""
validate_authentication(self)

def get_odbc_dsn_dict(self) -> Dict[str, Any]:
"""Build ODBC DSN dictionary with Fabric-specific settings."""
params = {
"DRIVER": "{ODBC Driver 18 for SQL Server}",
"""Build ODBC DSN dictionary with Fabric-specific settings.

mssql-python bundles its own driver, so no DRIVER key is emitted.
"""
params: dict[str, Any] = {
"SERVER": f"{self.host},{self.port}",
"DATABASE": self.database,
"AUTHENTICATION": "ActiveDirectoryServicePrincipal",
"LongAsMax": "yes", # Required for UTF-8 collation support
# The mssql-python driver handles long/max types (e.g. varchar(max)) natively,
# so no LongAsMax keyword is needed.
"Encrypt": "yes",
"TrustServerCertificate": "no",
}

# Add Service Principal credentials if provided
if self.azure_client_id and self.azure_tenant_id and self.azure_client_secret:
params["UID"] = f"{self.azure_client_id}@{self.azure_tenant_id}"
params["PWD"] = str(self.azure_client_secret)

apply_authentication_to_dsn(self, params)
return params

def to_odbc_attrs_before(self) -> dict[int, bytes] | None:
"""Return `attrs_before` with a directly injected Entra ID access token, or None."""
return build_token_attrs_before(self)

def to_odbc_dsn(self) -> str:
"""Build ODBC connection string for pyodbc."""
"""Build the ODBC connection string."""
params = self.get_odbc_dsn_dict()
return ";".join(f"{k}={v}" for k, v in params.items())

Expand Down Expand Up @@ -163,7 +183,7 @@ class FabricClientConfiguration(DestinationClientDwhWithStagingConfiguration):
- Latin1_General_100_BIN2_UTF8 (default, case-sensitive)
- Latin1_General_100_CI_AS_KS_WS_SC_UTF8 (case-insensitive)

Both have UTF-8 encoding. LongAsMax=yes is automatically configured.
Both have UTF-8 encoding. Long/max types are handled natively by the driver.
"""

def physical_location(self) -> str:
Expand Down
2 changes: 2 additions & 0 deletions dlt/destinations/impl/fabric/sql_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class FabricSqlClient(SynapseSqlClient):
"""SQL client for Microsoft Fabric Warehouse

Inherits all behavior from Synapse since Fabric Warehouse is built on Synapse technology.
Azure AD token injection (for token-based authentication) is handled by the shared
`PyOdbcMsSqlClient.open_connection`.
"""

def __init__(
Expand Down
Loading