Skip to content

Commit b6dd786

Browse files
authored
chore(deps): bump databricks-sdk and databricks-sql-connector ceilings (#1474)
Bump ceilings on `databricks-sdk` and `databricks-sql-connector` to admit newer versions. Floors unchanged. - `databricks-sdk`: `<0.78.0` → `<0.105.0` - `databricks-sql-connector[pyarrow]`: `<4.1.6` → `<4.3.0` - `packaging>=21.0, <26.0` declared explicitly (was transitive via `dbt-core`). - `uv.lock` refreshed to the latest within the new ranges.
1 parent 194c966 commit b6dd786

6 files changed

Lines changed: 218 additions & 29 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88

99
- Fix missing f-string prefix in `JobRunsApi.submit` debug log ([#1471](https://github.com/databricks/dbt-databricks/pull/1471))
1010

11+
### Under the Hood
12+
13+
- Defer SDK `Config` construction to connection-open time so offline paths (`dbt parse`/`list`/`compile`) don't trigger the host-metadata probe introduced in `databricks-sdk>=0.103`; as a side effect, auth errors now surface at first connection rather than during profile parsing. ([#1474](https://github.com/databricks/dbt-databricks/pull/1474))
14+
1115
## dbt-databricks 1.12.0 (May 18, 2026)
1216

1317
### Features

dbt/adapters/databricks/credentials.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -331,10 +331,15 @@ def authenticate_with_azure_client_secret(self) -> Config:
331331
)
332332

333333
def __post_init__(self) -> None:
334+
# Defer Config construction to first use so dbt parse/list/compile
335+
# stay offline.
334336
if not hasattr(self, "_config"):
335337
self._config: Optional[Config] = None
338+
339+
def _ensure_config(self) -> Config:
340+
"""Build (or return cached) SDK Config. Triggers authentication."""
336341
if self._config is not None:
337-
return
342+
return self._config
338343

339344
if self.token:
340345
self._config = self.authenticate_with_pat()
@@ -380,9 +385,13 @@ def __post_init__(self) -> None:
380385
)
381386
raise Exception(f"All authentication methods failed. Details: {exceptions}")
382387

388+
# Narrow Optional[Config] for the return type.
389+
assert self._config is not None
390+
return self._config
391+
383392
@property
384393
def api_client(self) -> WorkspaceClient:
385-
return WorkspaceClient(config=self._config)
394+
return WorkspaceClient(config=self._ensure_config())
386395

387396
@property
388397
def credentials_provider(self) -> PySQLCredentialProvider:
@@ -393,14 +402,10 @@ def inner() -> Callable[[], dict[str, str]]:
393402

394403
@property
395404
def header_factory(self) -> CredentialsProvider:
396-
if self._config is None:
397-
raise RuntimeError("Config is not initialized")
398-
header_factory = self._config._header_factory
405+
header_factory = self._ensure_config()._header_factory
399406
assert header_factory is not None, "Header factory is not set."
400407
return header_factory
401408

402409
@property
403410
def config(self) -> Config:
404-
if self._config is None:
405-
raise RuntimeError("Config is not initialized")
406-
return self._config
411+
return self._ensure_config()

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,14 @@ classifiers = [
2323
]
2424
dependencies = [
2525
"click>=8.2.0, <9.0.0",
26-
"databricks-sdk>=0.68.0, <0.78.0",
27-
"databricks-sql-connector[pyarrow]>=4.1.1, <4.1.6",
26+
"databricks-sdk>=0.68.0, <0.105.0",
27+
"databricks-sql-connector[pyarrow]>=4.1.1, <4.3.0",
2828
"dbt-adapters>=1.22.0, <1.23.0",
2929
"dbt-common>=1.37.0, <1.38.0",
3030
"dbt-core>=1.11.2, <1.11.9",
3131
"dbt-spark>=1.10.0, <1.11.0",
3232
"keyring>=23.13.0, <25.6.0",
33+
"packaging>=21.0, <26.0",
3334
"pydantic>=1.10.0, <2.13.0",
3435
]
3536

tests/unit/conftest.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""Unit-test-only fixtures."""
2+
3+
from contextlib import nullcontext
4+
from unittest import mock
5+
6+
import pytest
7+
from databricks.sdk import config as _sdk_config
8+
9+
# databricks-sdk >=0.103 added a network probe to Config(); on older SDKs
10+
# this symbol doesn't exist and there's nothing to stub.
11+
_SDK_HAS_HOST_METADATA = hasattr(_sdk_config, "get_host_metadata")
12+
13+
14+
@pytest.fixture(autouse=True)
15+
def _stub_sdk_host_metadata():
16+
"""Stub the SDK's host-metadata probe — unit tests must stay offline."""
17+
if not _SDK_HAS_HOST_METADATA:
18+
with nullcontext() as m:
19+
yield m
20+
return
21+
with mock.patch("databricks.sdk.config.get_host_metadata") as m:
22+
m.side_effect = ValueError("offline unit test")
23+
yield m

tests/unit/test_auth.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,156 @@
11
import os
22
import tempfile
33
from os.path import join
4+
from unittest import mock
45

56
import keyring.backend
67
import pytest
8+
from databricks.sdk import config as _sdk_config
79

810
from dbt.adapters.databricks.credentials import DatabricksCredentials
911

12+
# databricks-sdk >=0.103 added a network probe to Config(); on older SDKs
13+
# this symbol doesn't exist and the lazy `_ensure_config` path is unnecessary.
14+
_REQUIRES_LAZY_CONFIG = pytest.mark.skipif(
15+
not hasattr(_sdk_config, "get_host_metadata"),
16+
reason="lazy _ensure_config is only required when Config() does a network probe",
17+
)
18+
19+
20+
_COMMON_KWARGS = {
21+
"host": "yourorg.databricks.com",
22+
"database": "andre",
23+
"http_path": "sql/protocolv1/o/1234567890123456/1234-cluster",
24+
"schema": "dbt",
25+
}
26+
27+
28+
@_REQUIRES_LAZY_CONFIG
29+
class TestParseTimeIsOffline:
30+
"""`dbt parse/list/compile` must stay fully offline. Building
31+
`DatabricksCredentials` is on that path, so for every supported auth
32+
method we verify that constructing the credentials never calls
33+
`Config()` (which is what triggers the SDK's network I/O)."""
34+
35+
def test_pat_credentials_init_does_not_call_config(self):
36+
with mock.patch("dbt.adapters.databricks.credentials.Config") as mock_config:
37+
DatabricksCredentials(token="foo", **_COMMON_KWARGS)
38+
mock_config.assert_not_called()
39+
40+
def test_oauth_m2m_credentials_init_does_not_call_config(self):
41+
with mock.patch("dbt.adapters.databricks.credentials.Config") as mock_config:
42+
DatabricksCredentials(
43+
client_id="cid",
44+
client_secret="dose-secret",
45+
**_COMMON_KWARGS,
46+
)
47+
mock_config.assert_not_called()
48+
49+
def test_external_browser_credentials_init_does_not_call_config(self):
50+
# client_id with no client_secret triggers external-browser auth
51+
with mock.patch("dbt.adapters.databricks.credentials.Config") as mock_config:
52+
DatabricksCredentials(client_id="cid", **_COMMON_KWARGS)
53+
mock_config.assert_not_called()
54+
55+
def test_azure_client_secret_credentials_init_does_not_call_config(self):
56+
with mock.patch("dbt.adapters.databricks.credentials.Config") as mock_config:
57+
DatabricksCredentials(
58+
azure_client_id="acid",
59+
azure_client_secret="asecret",
60+
**_COMMON_KWARGS,
61+
)
62+
mock_config.assert_not_called()
63+
64+
65+
@_REQUIRES_LAZY_CONFIG
66+
class TestEnsureConfigTriggersTheRightAuth:
67+
"""Connect-time counterpart to TestParseTimeIsOffline: when something
68+
actually does need the config (e.g. opening a connection), `_ensure_config`
69+
must build it via the correct auth method per credential shape. This also
70+
exercises every branch of the selection logic in `_ensure_config`."""
71+
72+
def test_pat_uses_pat_auth(self):
73+
creds = DatabricksCredentials(token="foo", **_COMMON_KWARGS)
74+
with mock.patch("dbt.adapters.databricks.credentials.Config") as mock_config:
75+
creds.authenticate().config
76+
mock_config.assert_called_once_with(host=_COMMON_KWARGS["host"], token="foo")
77+
78+
def test_explicit_azure_uses_azure_client_secret(self):
79+
creds = DatabricksCredentials(
80+
azure_client_id="acid",
81+
azure_client_secret="asecret",
82+
auth_type="oauth",
83+
**_COMMON_KWARGS,
84+
)
85+
with mock.patch("dbt.adapters.databricks.credentials.Config") as mock_config:
86+
creds.authenticate().config
87+
mock_config.assert_called_once_with(
88+
host=_COMMON_KWARGS["host"],
89+
azure_client_id="acid",
90+
azure_client_secret="asecret",
91+
auth_type="azure-client-secret",
92+
)
93+
94+
def test_client_id_only_uses_external_browser(self):
95+
creds = DatabricksCredentials(client_id="cid", auth_type="oauth", **_COMMON_KWARGS)
96+
with mock.patch("dbt.adapters.databricks.credentials.Config") as mock_config:
97+
creds.authenticate().config
98+
mock_config.assert_called_once_with(
99+
host=_COMMON_KWARGS["host"],
100+
client_id="cid",
101+
client_secret="",
102+
auth_type="external-browser",
103+
)
104+
105+
def test_dose_secret_tries_oauth_m2m_first(self):
106+
creds = DatabricksCredentials(
107+
client_id="cid",
108+
client_secret="dose-secret",
109+
auth_type="oauth",
110+
**_COMMON_KWARGS,
111+
)
112+
with mock.patch("dbt.adapters.databricks.credentials.Config") as mock_config:
113+
creds.authenticate().config
114+
mock_config.assert_called_once_with(
115+
host=_COMMON_KWARGS["host"],
116+
client_id="cid",
117+
client_secret="dose-secret",
118+
auth_type="oauth-m2m",
119+
)
120+
121+
def test_non_dose_secret_tries_legacy_azure_first(self):
122+
creds = DatabricksCredentials(
123+
client_id="cid",
124+
client_secret="plain-secret",
125+
auth_type="oauth",
126+
**_COMMON_KWARGS,
127+
)
128+
with mock.patch("dbt.adapters.databricks.credentials.Config") as mock_config:
129+
creds.authenticate().config
130+
mock_config.assert_called_once_with(
131+
host=_COMMON_KWARGS["host"],
132+
azure_client_id="cid",
133+
azure_client_secret="plain-secret",
134+
auth_type="azure-client-secret",
135+
)
136+
137+
def test_falls_back_to_second_method_when_first_raises(self):
138+
creds = DatabricksCredentials(
139+
client_id="cid",
140+
client_secret="plain-secret",
141+
auth_type="oauth",
142+
**_COMMON_KWARGS,
143+
)
144+
fake_config = mock.MagicMock()
145+
with mock.patch("dbt.adapters.databricks.credentials.Config") as mock_config:
146+
mock_config.side_effect = [RuntimeError("first method fails"), fake_config]
147+
creds.authenticate().config
148+
assert mock_config.call_count == 2
149+
# First call: legacy-azure (default order for non-dose secret).
150+
assert mock_config.call_args_list[0].kwargs["auth_type"] == "azure-client-secret"
151+
# Second call: oauth-m2m fallback.
152+
assert mock_config.call_args_list[1].kwargs["auth_type"] == "oauth-m2m"
153+
10154

11155
@pytest.mark.skip(reason="Need to mock requests to OIDC")
12156
class TestM2MAuth:

0 commit comments

Comments
 (0)