Skip to content

Commit c07b31b

Browse files
committed
improve error logging
1 parent 3b1dca0 commit c07b31b

2 files changed

Lines changed: 106 additions & 2 deletions

File tree

libs/foundry-dev-tools/src/foundry_dev_tools/errors/sql.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from typing import TYPE_CHECKING
66

77
from foundry_dev_tools.errors.meta import FoundryAPIError
8+
from foundry_dev_tools.utils.misc import decamelize
89

910
if TYPE_CHECKING:
1011
import requests
@@ -16,8 +17,41 @@ class FoundrySqlQueryFailedError(FoundryAPIError):
1617
message = "Foundry SQL Query Failed."
1718

1819
def __init__(self, response: requests.Response):
19-
self.error_message = response.json().get("status", {}).get("failed", {}).get("errorMessage", "")
20-
super().__init__(response=response, info=self.error_message)
20+
kwargs = {}
21+
info = ""
22+
23+
try:
24+
response_json = response.json()
25+
failed_data = response_json.get("status", {}).get("failed", {})
26+
27+
# Try to extract V2 error structure with rich parameters
28+
if error_code := failed_data.get("errorCode"):
29+
kwargs["error_code"] = error_code
30+
if error_name := failed_data.get("errorName"):
31+
kwargs["error_name"] = error_name
32+
if error_instance_id := failed_data.get("errorInstanceId"):
33+
kwargs["error_instance_id"] = error_instance_id
34+
35+
# Extract all parameters and convert camelCase to snake_case
36+
if parameters := failed_data.get("parameters"):
37+
for key, value in parameters.items():
38+
kwargs[decamelize(key)] = value
39+
40+
# Prefer userFriendlyMessage as the info text
41+
info = parameters.get("userFriendlyMessage", "")
42+
43+
# Fall back to V1 errorMessage if userFriendlyMessage not available
44+
if not info:
45+
info = failed_data.get("errorMessage", "")
46+
47+
# Store legacy error_message attribute for backward compatibility
48+
self.error_message = info
49+
50+
except Exception: # noqa: BLE001
51+
# If any error occurs during extraction, fall back to empty
52+
self.error_message = ""
53+
54+
super().__init__(response=response, info=info, **kwargs)
2155

2256

2357
class FurnaceSqlSqlParseError(FoundryAPIError):

tests/unit/clients/test_foundry_sql_server.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,3 +233,73 @@ def test_v2_invalid_compression_codec(test_context_mock):
233233
"SELECT * FROM `ri.foundry.main.dataset.test-dataset`",
234234
arrow_compression_codec="INVALID", # type: ignore[arg-type]
235235
)
236+
237+
238+
def test_v2_query_failed_error_details(mocker, test_context_mock):
239+
"""Test that V2 error responses with rich parameters are properly extracted."""
240+
mocker.patch("time.sleep")
241+
242+
# Mock the api_query endpoint (initial query execution)
243+
test_context_mock.mock_adapter.register_uri(
244+
"POST",
245+
build_api_url(TEST_HOST.url, "foundry-sql-server", "sql-endpoint/v1/queries/query"),
246+
json={"type": "running", "running": {"queryHandle": {"queryId": "test-query-id", "type": "foundry"}}},
247+
)
248+
249+
# Mock the api_status endpoint with V2 error structure containing rich parameters
250+
test_context_mock.mock_adapter.register_uri(
251+
"POST",
252+
build_api_url(TEST_HOST.url, "foundry-sql-server", "sql-endpoint/v1/queries/status"),
253+
json={
254+
"status": {
255+
"type": "failed",
256+
"failed": {
257+
"errorCode": "INVALID_ARGUMENT",
258+
"errorName": "SqlQueryService:SqlSyntaxError",
259+
"errorInstanceId": "c16cb2b7-01ec-42a9-9ee2-0e57e2aed4ba",
260+
"parameters": {
261+
"endLine": 1,
262+
"endColumn": 15350,
263+
"dialect": "SPARK",
264+
"queryFragment": "",
265+
"startColumn": 15340,
266+
"startLine": 1,
267+
"userFriendlyMessage": (
268+
"From line 1, column 15340 to line 1, column 15350: "
269+
"Column 'COLUMN_NAME' not found in table 'my_table'; did you mean 'column_name'?"
270+
),
271+
},
272+
},
273+
}
274+
},
275+
)
276+
277+
with pytest.raises(FoundrySqlQueryFailedError) as exception:
278+
test_context_mock.foundry_sql_server_v2.query_foundry_sql(
279+
"SELECT COLUMN_NAME FROM `ri.foundry.main.dataset.test-dataset`",
280+
)
281+
282+
# Verify all error parameters are extracted and accessible
283+
assert exception.value.error_code == "INVALID_ARGUMENT"
284+
assert exception.value.error_name == "SqlQueryService:SqlSyntaxError"
285+
assert exception.value.error_instance_id == "c16cb2b7-01ec-42a9-9ee2-0e57e2aed4ba"
286+
287+
# Verify parameters are converted from camelCase to snake_case and accessible
288+
assert exception.value.start_line == 1
289+
assert exception.value.end_line == 1
290+
assert exception.value.start_column == 15340
291+
assert exception.value.end_column == 15350
292+
assert exception.value.dialect == "SPARK"
293+
# query_fragment is in kwargs even if empty
294+
assert "query_fragment" in exception.value.kwargs
295+
296+
# Verify userFriendlyMessage is used as the info text and accessible
297+
assert exception.value.user_friendly_message == (
298+
"From line 1, column 15340 to line 1, column 15350: "
299+
"Column 'COLUMN_NAME' not found in table 'my_table'; did you mean 'column_name'?"
300+
)
301+
302+
# Verify the exception message string includes the user-friendly message
303+
exception_str = str(exception.value)
304+
assert "COLUMN_NAME" in exception_str
305+
assert "my_table" in exception_str

0 commit comments

Comments
 (0)