Skip to content

Commit 5a6798a

Browse files
99Lysclaude
andauthored
Suppress Dremio Cloud 400 "already exists" in folder creation (#318)
## Summary Dremio Cloud returns HTTP 400 with `"An object already exists with that name"` when a folder already exists during `_create_folders`, whereas Dremio Software returns HTTP 409. The Software 409 case is already swallowed by the existing `DremioAlreadyExistsException` handler in `_create_folders`, but the Cloud 400 case fell into the `DremioBadRequestException` branch and crashed `dbt run`. This PR normalizes at the API boundary: `_check_error` in `dbt/adapters/dremio/api/rest/utils.py` now reroutes 400 + `"An object already exists with that name"` to `DremioAlreadyExistsException`. The existing handlers in `_create_folders` and `_create_space` catch both Cloud and Software cases uniformly with no changes. ### Why at this layer - Centralizes the Cloud-vs-Software server quirk at the API boundary. - Future callers of any catalog API automatically benefit. - Exception type matches its semantic meaning (already-exists, regardless of HTTP status code). - `connections.py` stays unchanged — no scope creep. ## Test Plan - [x] `pytest tests/unit/` — 8/8 tests pass - [x] `tests/unit/test_rest_utils.py` (new) — `_check_error` mapping: - 400 + "already exists" → `DremioAlreadyExistsException` ✓ - 400 + other message → `DremioBadRequestException` (regression guard) ✓ - 409 → `DremioAlreadyExistsException` (regression guard) ✓ - [x] `tests/unit/test_connection.py::TestCreateFolders::test_create_folders_swallows_already_exists` (new) — end-to-end: `_create_folders` swallows the rerouted exception, loop continues, method returns normally. - [x] `black` + `flake8` clean on touched files. ## Risk The match string `"An object already exists with that name"` is a literal substring against the response body. If Cloud ever changes the wording, the fallback path raises `DremioBadRequestException` and `dbt run` crashes (current baseline). Acceptable trade-off; we can add another substring if needed. ## References - Original bug report: #313 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent eac8389 commit 5a6798a

3 files changed

Lines changed: 92 additions & 2 deletions

File tree

dbt/adapters/dremio/api/rest/utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,13 @@ def _check_error(response, details=""):
120120
except: # NOQA
121121
return response.text
122122
if code == 400:
123+
# Dremio Cloud returns 400 for an existing folder/object, whereas
124+
# Software returns 409. Normalize so callers can catch a single
125+
# DremioAlreadyExistsException.
126+
if "An object already exists with that name" in response.text:
127+
raise DremioAlreadyExistsException(
128+
"Already exists:" + details, error, response
129+
)
123130
raise DremioBadRequestException("Bad request:" + details, error,
124131
response)
125132
if code == 401:

tests/unit/test_connection.py

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@
1010
# limitations under the License.
1111

1212
import pytest
13-
from unittest.mock import patch
13+
from unittest.mock import MagicMock, patch
1414
from dbt_common.exceptions import DbtRuntimeError
15-
from dbt.adapters.dremio.api.rest.error import DremioRequestTimeoutException
15+
from dbt.adapters.dremio.api.rest.error import (
16+
DremioAlreadyExistsException,
17+
DremioRequestTimeoutException,
18+
)
1619
from dbt.adapters.dremio.connections import DremioConnectionManager
1720

1821

@@ -46,3 +49,22 @@ def test_connection_retry(
4649

4750
# Assert
4851
assert mocked_post_func.call_count == TOTAL_CONNECTION_ATTEMPTS
52+
53+
54+
class TestCreateFolders:
55+
def test_create_folders_swallows_already_exists(self):
56+
# Guards the integration path: _check_error normalizes Cloud's
57+
# 400 "already exists" to DremioAlreadyExistsException, which the
58+
# existing handler in _create_folders is responsible for swallowing.
59+
mgr = DremioConnectionManager.__new__(DremioConnectionManager)
60+
mgr._make_new_folder_json = MagicMock(return_value="{}")
61+
rest_client = MagicMock()
62+
rest_client.create_catalog_api.side_effect = DremioAlreadyExistsException(
63+
msg="Already exists:",
64+
original_exception="400 Client Error: Bad Request",
65+
)
66+
67+
mgr._create_folders("mySource", "staging.subfolder", rest_client)
68+
69+
# Both folders in the path attempted; both swallowed
70+
assert rest_client.create_catalog_api.call_count == 2

tests/unit/test_rest_utils.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Copyright (C) 2022 Dremio Corporation
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
# Unless required by applicable law or agreed to in writing, software
7+
# distributed under the License is distributed on an "AS IS" BASIS,
8+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
# See the License for the specific language governing permissions and
10+
# limitations under the License.
11+
12+
import json
13+
from unittest.mock import MagicMock
14+
15+
import pytest
16+
17+
from dbt.adapters.dremio.api.rest.error import (
18+
DremioAlreadyExistsException,
19+
DremioBadRequestException,
20+
)
21+
from dbt.adapters.dremio.api.rest.utils import _check_error
22+
23+
24+
_REASON_BY_STATUS = {400: "Bad Request", 409: "Conflict"}
25+
26+
27+
def _make_response(status_code, body_text):
28+
response = MagicMock()
29+
response.status_code = status_code
30+
response.reason = _REASON_BY_STATUS.get(status_code, "Bad Request")
31+
response.url = "https://api.dremio.cloud/v0/projects/test/catalog"
32+
response.text = body_text
33+
return response
34+
35+
36+
class TestCheckErrorAlreadyExists:
37+
def test_cloud_400_already_exists_routes_to_already_exists_exception(self):
38+
body = json.dumps(
39+
{
40+
"errorMessage": (
41+
"Unable to create folder staging on source mySource. "
42+
"An object already exists with that name."
43+
),
44+
"moreInfo": "",
45+
}
46+
)
47+
response = _make_response(400, body)
48+
with pytest.raises(DremioAlreadyExistsException):
49+
_check_error(response)
50+
51+
def test_400_other_message_still_raises_bad_request(self):
52+
body = json.dumps({"errorMessage": "Some other 400 error", "moreInfo": ""})
53+
response = _make_response(400, body)
54+
with pytest.raises(DremioBadRequestException):
55+
_check_error(response)
56+
57+
def test_409_still_raises_already_exists(self):
58+
body = json.dumps({"errorMessage": "Already exists", "moreInfo": ""})
59+
response = _make_response(409, body)
60+
with pytest.raises(DremioAlreadyExistsException):
61+
_check_error(response)

0 commit comments

Comments
 (0)