diff --git a/dbt/adapters/dremio/api/rest/utils.py b/dbt/adapters/dremio/api/rest/utils.py index a90cc8cb..3a0c723e 100644 --- a/dbt/adapters/dremio/api/rest/utils.py +++ b/dbt/adapters/dremio/api/rest/utils.py @@ -120,6 +120,13 @@ def _check_error(response, details=""): except: # NOQA return response.text if code == 400: + # Dremio Cloud returns 400 for an existing folder/object, whereas + # Software returns 409. Normalize so callers can catch a single + # DremioAlreadyExistsException. + if "An object already exists with that name" in response.text: + raise DremioAlreadyExistsException( + "Already exists:" + details, error, response + ) raise DremioBadRequestException("Bad request:" + details, error, response) if code == 401: diff --git a/tests/unit/test_connection.py b/tests/unit/test_connection.py index 37153cc2..c0c0ba04 100644 --- a/tests/unit/test_connection.py +++ b/tests/unit/test_connection.py @@ -10,9 +10,12 @@ # limitations under the License. import pytest -from unittest.mock import patch +from unittest.mock import MagicMock, patch from dbt_common.exceptions import DbtRuntimeError -from dbt.adapters.dremio.api.rest.error import DremioRequestTimeoutException +from dbt.adapters.dremio.api.rest.error import ( + DremioAlreadyExistsException, + DremioRequestTimeoutException, +) from dbt.adapters.dremio.connections import DremioConnectionManager @@ -46,3 +49,22 @@ def test_connection_retry( # Assert assert mocked_post_func.call_count == TOTAL_CONNECTION_ATTEMPTS + + +class TestCreateFolders: + def test_create_folders_swallows_already_exists(self): + # Guards the integration path: _check_error normalizes Cloud's + # 400 "already exists" to DremioAlreadyExistsException, which the + # existing handler in _create_folders is responsible for swallowing. + mgr = DremioConnectionManager.__new__(DremioConnectionManager) + mgr._make_new_folder_json = MagicMock(return_value="{}") + rest_client = MagicMock() + rest_client.create_catalog_api.side_effect = DremioAlreadyExistsException( + msg="Already exists:", + original_exception="400 Client Error: Bad Request", + ) + + mgr._create_folders("mySource", "staging.subfolder", rest_client) + + # Both folders in the path attempted; both swallowed + assert rest_client.create_catalog_api.call_count == 2 diff --git a/tests/unit/test_rest_utils.py b/tests/unit/test_rest_utils.py new file mode 100644 index 00000000..2785fdf6 --- /dev/null +++ b/tests/unit/test_rest_utils.py @@ -0,0 +1,61 @@ +# Copyright (C) 2022 Dremio Corporation +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# http://www.apache.org/licenses/LICENSE-2.0 +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +from unittest.mock import MagicMock + +import pytest + +from dbt.adapters.dremio.api.rest.error import ( + DremioAlreadyExistsException, + DremioBadRequestException, +) +from dbt.adapters.dremio.api.rest.utils import _check_error + + +_REASON_BY_STATUS = {400: "Bad Request", 409: "Conflict"} + + +def _make_response(status_code, body_text): + response = MagicMock() + response.status_code = status_code + response.reason = _REASON_BY_STATUS.get(status_code, "Bad Request") + response.url = "https://api.dremio.cloud/v0/projects/test/catalog" + response.text = body_text + return response + + +class TestCheckErrorAlreadyExists: + def test_cloud_400_already_exists_routes_to_already_exists_exception(self): + body = json.dumps( + { + "errorMessage": ( + "Unable to create folder staging on source mySource. " + "An object already exists with that name." + ), + "moreInfo": "", + } + ) + response = _make_response(400, body) + with pytest.raises(DremioAlreadyExistsException): + _check_error(response) + + def test_400_other_message_still_raises_bad_request(self): + body = json.dumps({"errorMessage": "Some other 400 error", "moreInfo": ""}) + response = _make_response(400, body) + with pytest.raises(DremioBadRequestException): + _check_error(response) + + def test_409_still_raises_already_exists(self): + body = json.dumps({"errorMessage": "Already exists", "moreInfo": ""}) + response = _make_response(409, body) + with pytest.raises(DremioAlreadyExistsException): + _check_error(response)