Skip to content

Commit

Permalink
Cast database errors to click exceptions for UX (#1522)
Browse files Browse the repository at this point in the history
  • Loading branch information
sfc-gh-turbaszek committed Sep 12, 2024
1 parent fd1bb79 commit 026f635
Show file tree
Hide file tree
Showing 10 changed files with 79 additions and 63 deletions.
8 changes: 6 additions & 2 deletions src/snowflake/cli/api/commands/snow_typer.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from snowflake.cli.api.output.types import CommandResult
from snowflake.cli.api.sanitizers import sanitize_for_terminal
from snowflake.cli.api.sql_execution import SqlExecutionMixin
from snowflake.connector import DatabaseError

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -106,8 +107,8 @@ def command_callable_decorator(*args, **kw):
execution.complete(ExecutionStatus.SUCCESS)
except Exception as err:
execution.complete(ExecutionStatus.FAILURE)
self.exception_handler(err, execution)
raise
exception = self.exception_handler(err, execution)
raise exception
finally:
self.post_execute(execution)

Expand Down Expand Up @@ -155,6 +156,9 @@ def exception_handler(exception: Exception, execution: ExecutionMetadata):

log.debug("Executing command exception callback")
log_command_execution_error(exception, execution)
if isinstance(exception, DatabaseError):
return ClickException(exception.msg)
return exception

@staticmethod
def post_execute(execution: ExecutionMetadata):
Expand Down
19 changes: 10 additions & 9 deletions tests/stage/test_stage.py
Original file line number Diff line number Diff line change
Expand Up @@ -957,16 +957,17 @@ def test_execute_not_existing_stage(mock_execute, mock_cursor, runner):
)
]

with pytest.raises(ProgrammingError) as e:
runner.invoke(["stage", "execute", stage_name])
result = runner.invoke(["stage", "execute", stage_name])

assert result.exit_code == 1
assert (
f"002003: 2003: Stage '{stage_name}' does not exist or not authorized."
in result.output
)

assert mock_execute.mock_calls == [
mock.call(f"ls @{stage_name}", cursor_class=DictCursor)
]
assert (
e.value.msg
== f"002003: 2003: Stage '{stage_name}' does not exist or not authorized."
)


@pytest.mark.parametrize(
Expand Down Expand Up @@ -1016,8 +1017,8 @@ def test_execute_stop_on_error(mock_bootstrap, mock_execute, mock_cursor, runner
ProgrammingError(error_message),
]

with pytest.raises(ProgrammingError) as e:
runner.invoke(["stage", "execute", "exe"])
result = runner.invoke(["stage", "execute", "exe"])
assert result.exit_code == 1

assert mock_execute.mock_calls == [
mock.call("ls @exe", cursor_class=DictCursor),
Expand All @@ -1028,7 +1029,7 @@ def test_execute_stop_on_error(mock_bootstrap, mock_execute, mock_cursor, runner
mock.call("@exe/p1.py", {}),
mock.call("@exe/p2.py", {}),
]
assert e.value.msg == error_message
assert error_message in result.output


@mock.patch(f"{STAGE_MANAGER}._execute_query")
Expand Down
1 change: 1 addition & 0 deletions tests_e2e/config/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
[connections.dev]
[connections.integration]
schema = "public"
authenticator = "SNOWFLAKE_JWT"


[cli.plugins.snowpark-hello]
Expand Down
23 changes: 3 additions & 20 deletions tests_e2e/test_error_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,26 +38,9 @@ def test_error_traceback_disabled_without_debug(snowcli, test_root_path, config_
]
)

assert "SQL compilation error" in result.stdout
assert traceback_msg not in result.stdout
assert not result.stderr

result_debug = subprocess_run(
[
snowcli,
"--config-file",
config_file,
"sql",
"-c",
"integration",
"-q",
"select foo",
"--debug",
]
)

assert result_debug.stdout == "select foo\n"
assert traceback_msg in result_debug.stderr
assert "SQL compilation error" in result.stderr
assert traceback_msg not in result.stderr
assert result.stdout == "select foo\n"


@pytest.mark.e2e
Expand Down
20 changes: 20 additions & 0 deletions tests_integration/__snapshots__/test_git.ambr
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
# serializer version: 1
# name: test_copy_error
'''
093554 (22023): 01b6ce5e-090b-6508-0001-c1be067d48f6: The specified tag 'no-such-tag' cannot be found in the Git Repository.
╭─ Error ──────────────────────────────────────────────────────────────────────╮
│ 093554 (22023): 01b6ce5e-090b-6508-0001-c1be067d48f6: The specified tag │
│ 'no-such-tag' cannot be found in the Git Repository. │
╰──────────────────────────────────────────────────────────────────────────────╯

'''
# ---
# name: test_execute
list([
dict({
Expand All @@ -17,3 +27,13 @@
}),
])
# ---
# name: test_list_files
'''
093554 (22023): 01b6ce5e-090b-6508-0001-c1be067d48be: The specified tag 'no-such-tag' cannot be found in the Git Repository.
╭─ Error ──────────────────────────────────────────────────────────────────────╮
│ 093554 (22023): 01b6ce5e-090b-6508-0001-c1be067d48be: The specified tag │
│ 'no-such-tag' cannot be found in the Git Repository. │
╰──────────────────────────────────────────────────────────────────────────────╯

'''
# ---
7 changes: 6 additions & 1 deletion tests_integration/__snapshots__/test_sql.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@
{
"2" : 2
}
]
]╭─ Error ──────────────────────────────────────────────────────────────────────╮
│ 000904 (42000): 01b6cd91-090b-6508-0001-c1be0678ca3a: SQL compilation error: │
│ error line 1 at position 7 │
│ invalid identifier 'FOO' │
╰──────────────────────────────────────────────────────────────────────────────╯

'''
# ---
# name: test_multiple_files
Expand Down
13 changes: 13 additions & 0 deletions tests_integration/notebook/__snapshots__/test_notebooks.ambr
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# serializer version: 1
# name: test_create_notebook
'''
╭─ Error ──────────────────────────────────────────────────────────────────────╮
│ 100357 (P0000): 01b6c2fb-090b-2340-0001-c1be063161de: Python Interpreter │
│ Error: │
│ Exception: NameError: name 'fooBar' is not defined │
│ File "Cell [None]", line 1, in <module> │
│ fooBar │
╰──────────────────────────────────────────────────────────────────────────────╯

'''
# ---
11 changes: 4 additions & 7 deletions tests_integration/notebook/test_notebooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,13 @@ def _execute_notebook(runner, notebook_name):


def _execute_notebook_failure(runner, notebook_name):
with pytest.raises(ProgrammingError) as err:
result = runner.invoke_with_connection_json(
["notebook", "execute", notebook_name, "--format", "json"]
)
assert result.exit_code == 1
assert "invalid identifier 'FOO'" in err
result = runner.invoke_with_connection(["notebook", "execute", notebook_name])
assert result.exit_code == 1
assert "NameError: name 'fooBar' is not defined" in result.output


@pytest.mark.integration
def test_create_notebook(runner, test_database, snowflake_session):
def test_create_notebook(runner, test_database, snowflake_session, snapshot):
stage_name = "notebook_stage"
snowflake_session.execute_string(f"create stage {stage_name};")

Expand Down
32 changes: 11 additions & 21 deletions tests_integration/test_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,15 +121,10 @@ def test_list_files(runner, sf_git_repository):
result = runner.invoke_with_connection(["git", "list-files", repository_path])
_assert_invalid_repo_path_error_message(result.output)

try:
repository_path = f"@{sf_git_repository}/tags/no-such-tag/"
runner.invoke_with_connection(["git", "list-files", repository_path])
assert False, "Expected exception"
except ProgrammingError as err:
assert (
err.raw_msg
== "The specified tag 'no-such-tag' cannot be found in the Git Repository."
)
repository_path = f"@{sf_git_repository}/tags/no-such-tag/"
result = runner.invoke_with_connection(["git", "list-files", repository_path])
assert result.exit_code == 1
assert "'no-such-tag' cannot be found" in result.output

# list files with pattern
repository_path = f"@{sf_git_repository}/tags/v2.1.0-rc1/"
Expand Down Expand Up @@ -246,18 +241,13 @@ def test_copy_single_file_to_local_file_system(runner, sf_git_repository):
def test_copy_error(runner, sf_git_repository):
with tempfile.TemporaryDirectory() as tmp_dir:
LOCAL_DIR = Path(tmp_dir) / "a_dir"
# error messages are passed to the user
try:
repository_path = f"@{sf_git_repository}/tags/no-such-tag/"
runner.invoke_with_connection(
["git", "copy", repository_path, str(LOCAL_DIR)]
)
assert False, "Expected exception"
except ProgrammingError as err:
assert (
err.raw_msg
== "The specified tag 'no-such-tag' cannot be found in the Git Repository."
)

repository_path = f"@{sf_git_repository}/tags/no-such-tag/"
result = runner.invoke_with_connection(
["git", "copy", repository_path, str(LOCAL_DIR)]
)
assert result.exit_code == 1
assert "'no-such-tag' cannot be found" in result.output


@pytest.mark.integration
Expand Down
8 changes: 5 additions & 3 deletions tests_integration/test_sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,14 +65,16 @@ def test_multiple_files(runner, snowflake_session, test_root_path, snapshot):

@pytest.mark.integration
def test_multi_queries_where_one_of_them_is_failing(
runner, snowflake_session, test_root_path, snapshot
runner, snowflake_session, test_root_path
):
result = runner.invoke_with_connection_json(
["sql", "-q", f"select 1; select 2; select foo; select 4", "--format", "json"],
catch_exceptions=True,
)
assert result.exit_code == 1

assert result.output == snapshot
assert '"1" : 1' in result.output
assert '"2" : 2' in result.output
assert "invalid identifier 'FOO'" in result.output


@pytest.mark.integration
Expand Down

0 comments on commit 026f635

Please sign in to comment.