Skip to content

Commit 3b27ca8

Browse files
committed
fix(adapters/databricks/connections.py): fix capability detection for named compute
1 parent 5931c21 commit 3b27ca8

2 files changed

Lines changed: 86 additions & 17 deletions

File tree

dbt/adapters/databricks/connections.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,23 @@ def _cache_dbr_capabilities(cls, creds: DatabricksCredentials, http_path: str) -
208208
is_sql_warehouse=not is_cluster,
209209
)
210210

211+
@classmethod
212+
def _try_cache_dbr_capabilities(cls, creds: DatabricksCredentials, http_path: str) -> None:
213+
"""Like _cache_dbr_capabilities, but only writes to the cache when the version query
214+
actually succeeds. This prevents a failed eager lookup (e.g. credentials_manager not yet
215+
set, cluster still spinning up) from storing a None-version entry that the idempotency
216+
guard in open() would later treat as authoritative, causing all capability checks to
217+
return False.
218+
"""
219+
if http_path not in cls._dbr_capabilities_cache:
220+
is_cluster = is_cluster_http_path(http_path, creds.cluster_id)
221+
dbr_version = cls._query_dbr_version(creds, http_path)
222+
if dbr_version is not None:
223+
cls._dbr_capabilities_cache[http_path] = DBRCapabilities(
224+
dbr_version=dbr_version,
225+
is_sql_warehouse=not is_cluster,
226+
)
227+
211228
def cancel_open(self) -> list[str]:
212229
cancelled = super().cancel_open()
213230
logger.info("Cancelling open python jobs")
@@ -288,7 +305,7 @@ def _create_fresh_connection(
288305
conn.http_path = QueryConfigUtils.get_http_path(query_header_context, creds)
289306
conn.thread_identifier = cast(tuple[int, int], self.get_thread_identifier())
290307
conn._query_header_context = query_header_context
291-
self._cache_dbr_capabilities(creds, conn.http_path)
308+
self._try_cache_dbr_capabilities(creds, conn.http_path)
292309
conn.capabilities = self._get_capabilities_for_http_path(conn.http_path)
293310
conn.handle = LazyHandle(self.open)
294311

tests/unit/test_connection_manager.py

Lines changed: 68 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -124,51 +124,103 @@ def connection_manager(self):
124124
DatabricksConnectionManager._dbr_capabilities_cache = {}
125125
return mgr
126126

127-
@patch.object(DatabricksConnectionManager, "_cache_dbr_capabilities")
127+
@patch.object(DatabricksConnectionManager, "_try_cache_dbr_capabilities")
128128
def test_fresh_connection_caches_capabilities_before_setting_them(
129-
self, mock_cache_dbr, connection_manager
129+
self, mock_try_cache, connection_manager
130130
):
131-
"""_create_fresh_connection must call _cache_dbr_capabilities before reading the cache."""
131+
"""_create_fresh_connection must call _try_cache_dbr_capabilities before reading the
132+
cache."""
132133
expected_caps = DBRCapabilities(dbr_version=(15, 4), is_sql_warehouse=False)
133134

134135
def populate_cache(creds, http_path):
135136
DatabricksConnectionManager._dbr_capabilities_cache[http_path] = expected_caps
136137

137-
mock_cache_dbr.side_effect = populate_cache
138+
mock_try_cache.side_effect = populate_cache
138139

139140
ctx = QueryContextWrapper(compute_name="alternate_compute")
140141
conn = connection_manager._create_fresh_connection("test_model", ctx)
141142

142-
mock_cache_dbr.assert_called_once_with(
143+
mock_try_cache.assert_called_once_with(
143144
connection_manager.profile.credentials, self.NAMED_COMPUTE_HTTP_PATH
144145
)
145146
assert conn.capabilities == expected_caps
146147
assert conn.capabilities.has_capability(DBRCapability.ICEBERG)
147148

148-
@patch.object(DatabricksConnectionManager, "_cache_dbr_capabilities")
149-
def test_fresh_connection_without_eager_cache_gets_empty_defaults(
150-
self, mock_cache_dbr, connection_manager
149+
@patch.object(DatabricksConnectionManager, "_try_cache_dbr_capabilities")
150+
def test_fresh_connection_falls_back_to_empty_defaults_when_eager_cache_is_noop(
151+
self, mock_try_cache, connection_manager
151152
):
152-
"""Without the eager cache call, named compute gets empty (broken) capabilities.
153-
154-
This test documents the pre-fix behavior: if _cache_dbr_capabilities is a no-op,
155-
the connection falls back to DBRCapabilities() which has all capabilities disabled.
156-
"""
153+
"""When the eager try-cache is a no-op (e.g. version query not yet possible), the
154+
connection gets default capabilities. open() will populate the cache correctly later."""
157155
ctx = QueryContextWrapper(compute_name="alternate_compute")
158156
conn = connection_manager._create_fresh_connection("test_model", ctx)
159157

160158
assert conn.capabilities.dbr_version is None
161159
assert not conn.capabilities.is_sql_warehouse
162160
assert not conn.capabilities.has_capability(DBRCapability.ICEBERG)
163161

164-
@patch.object(DatabricksConnectionManager, "_cache_dbr_capabilities")
165-
def test_default_compute_cache_is_idempotent(self, mock_cache_dbr, connection_manager):
162+
@patch.object(DatabricksConnectionManager, "_try_cache_dbr_capabilities")
163+
def test_default_compute_cache_is_idempotent(self, mock_try_cache, connection_manager):
166164
"""For default compute with a warm cache, the eager call is a no-op."""
167165
warm_caps = DBRCapabilities(dbr_version=(15, 4), is_sql_warehouse=False)
168166
DatabricksConnectionManager._dbr_capabilities_cache[self.DEFAULT_HTTP_PATH] = warm_caps
169167

170168
ctx = QueryContextWrapper()
171169
conn = connection_manager._create_fresh_connection("test_model", ctx)
172170

173-
mock_cache_dbr.assert_called_once()
171+
mock_try_cache.assert_called_once()
174172
assert conn.capabilities == warm_caps
173+
174+
175+
class TestTryCacheDbr:
176+
"""Unit tests for _try_cache_dbr_capabilities."""
177+
178+
HTTP_PATH = "sql/protocolv1/o/1234567890123456/cluster-abc"
179+
180+
@pytest.fixture(autouse=True)
181+
def clear_cache(self):
182+
DatabricksConnectionManager._dbr_capabilities_cache = {}
183+
yield
184+
DatabricksConnectionManager._dbr_capabilities_cache = {}
185+
186+
@patch.object(DatabricksConnectionManager, "_query_dbr_version", return_value=None)
187+
def test_does_not_write_to_cache_when_version_is_none(self, mock_query):
188+
"""When the version query returns None, the cache must not be written.
189+
190+
This prevents a poisoned None entry from blocking the authoritative write in open().
191+
"""
192+
creds = Mock(spec=DatabricksCredentials)
193+
creds.cluster_id = None
194+
195+
DatabricksConnectionManager._try_cache_dbr_capabilities(creds, self.HTTP_PATH)
196+
197+
assert self.HTTP_PATH not in DatabricksConnectionManager._dbr_capabilities_cache
198+
mock_query.assert_called_once_with(creds, self.HTTP_PATH)
199+
200+
@patch.object(DatabricksConnectionManager, "_query_dbr_version", return_value=(15, 4))
201+
def test_writes_to_cache_when_version_is_known(self, mock_query):
202+
"""When the version query succeeds, capabilities are cached correctly."""
203+
creds = Mock(spec=DatabricksCredentials)
204+
creds.cluster_id = None
205+
206+
DatabricksConnectionManager._try_cache_dbr_capabilities(creds, self.HTTP_PATH)
207+
208+
mock_query.assert_called_once_with(creds, self.HTTP_PATH)
209+
caps = DatabricksConnectionManager._dbr_capabilities_cache.get(self.HTTP_PATH)
210+
assert caps is not None
211+
assert caps.dbr_version == (15, 4)
212+
assert not caps.is_sql_warehouse
213+
assert caps.has_capability(DBRCapability.ICEBERG)
214+
215+
@patch.object(DatabricksConnectionManager, "_query_dbr_version", return_value=(15, 4))
216+
def test_skips_write_when_already_cached(self, mock_query):
217+
"""If the path is already in cache, the version query is never made."""
218+
creds = Mock(spec=DatabricksCredentials)
219+
creds.cluster_id = None
220+
existing = DBRCapabilities(dbr_version=(14, 3), is_sql_warehouse=False)
221+
DatabricksConnectionManager._dbr_capabilities_cache[self.HTTP_PATH] = existing
222+
223+
DatabricksConnectionManager._try_cache_dbr_capabilities(creds, self.HTTP_PATH)
224+
225+
mock_query.assert_not_called()
226+
assert DatabricksConnectionManager._dbr_capabilities_cache[self.HTTP_PATH] is existing

0 commit comments

Comments
 (0)