Skip to content

Commit 7a76b3f

Browse files
Feat: Include stack trace in rendering exception handling
1 parent d67cf25 commit 7a76b3f

2 files changed

Lines changed: 61 additions & 9 deletions

File tree

sqlmesh/core/renderer.py

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import logging
4+
import traceback
45
import typing as t
56
from contextlib import contextmanager
67
from functools import partial
@@ -211,12 +212,16 @@ def _resolve_table(table: str | exp.Table) -> str:
211212
expressions = [e for e in parse(rendered_expression, read=self._dialect) if e]
212213

213214
if not expressions:
214-
raise ConfigError(f"Failed to parse an expression:\n{self._expression}")
215+
raise ConfigError(
216+
f"{traceback.format_exc()}"
217+
+ f"Failed to parse an expression:\n{self._expression}"
218+
)
215219
except ParsetimeAdapterCallError:
216220
raise
217221
except Exception as ex:
218222
raise ConfigError(
219-
f"Could not render or parse jinja at '{self._path}'.\n{ex}"
223+
f"{traceback.format_exc()}"
224+
+ f"Could not render or parse jinja at '{self._path}'.\n{ex}"
220225
) from ex
221226

222227
macro_evaluator.locals.update(render_kwargs)
@@ -228,7 +233,10 @@ def _resolve_table(table: str | exp.Table) -> str:
228233
try:
229234
macro_evaluator.evaluate(definition)
230235
except Exception as ex:
231-
raise_config_error(f"Failed to evaluate macro '{definition}'. {ex}", self._path)
236+
raise_config_error(
237+
f"{traceback.format_exc()}" + f"Failed to evaluate macro '{definition}'. {ex}",
238+
self._path,
239+
)
232240

233241
resolved_expressions: t.List[t.Optional[exp.Expression]] = []
234242

@@ -237,7 +245,8 @@ def _resolve_table(table: str | exp.Table) -> str:
237245
transformed_expressions = ensure_list(macro_evaluator.transform(expression))
238246
except Exception as ex:
239247
raise_config_error(
240-
f"Failed to resolve macros for\n{expression.sql(dialect=self._dialect, pretty=True)}\n{ex}",
248+
f"{traceback.format_exc()}"
249+
+ f"Failed to resolve macros for\n{expression.sql(dialect=self._dialect, pretty=True)}\n{ex}",
241250
self._path,
242251
)
243252

@@ -532,18 +541,26 @@ def render(
532541
expressions = [e for e in expressions if not isinstance(e, exp.Semicolon)]
533542

534543
if not expressions:
535-
raise ConfigError(f"Failed to render query at '{self._path}':\n{self._expression}")
544+
raise ConfigError(
545+
f"{traceback.format_exc()}"
546+
+ f"Failed to render query at '{self._path}':\n{self._expression}"
547+
)
536548

537549
if len(expressions) > 1:
538-
raise ConfigError(f"Too many statements in query:\n{self._expression}")
550+
raise ConfigError(
551+
f"{traceback.format_exc()}"
552+
+ f"Too many statements in query:\n{self._expression}"
553+
)
539554

540555
query = expressions[0] # type: ignore
541556

542557
if not query:
543558
return None
544559
if not isinstance(query, exp.Query):
545560
raise_config_error(
546-
f"Model query needs to be a SELECT or a UNION, got {query}.", self._path
561+
f"{traceback.format_exc()}"
562+
+ f"Model query needs to be a SELECT or a UNION, got {query}.",
563+
self._path,
547564
)
548565
raise
549566

@@ -581,7 +598,9 @@ def update_cache(
581598
) -> None:
582599
if optimized:
583600
if not isinstance(expression, exp.Query):
584-
raise SQLMeshError(f"Expected a Query but got: {expression}")
601+
raise SQLMeshError(
602+
f"{traceback.format_exc()}" + f"Expected a Query but got: {expression}"
603+
)
585604
self._optimized_cache = expression
586605
else:
587606
super().update_cache(expression)
@@ -634,7 +653,8 @@ def _optimize_query(self, query: exp.Query, all_deps: t.Set[str]) -> exp.Query:
634653

635654
except Exception as ex:
636655
raise_config_error(
637-
f"Failed to optimize query, please file an issue at https://github.com/TobikoData/sqlmesh/issues/new. {ex}",
656+
f"{traceback.format_exc()}"
657+
+ f"Failed to optimize query, please file an issue at https://github.com/TobikoData/sqlmesh/issues/new. {ex}",
638658
self._path,
639659
)
640660

tests/core/test_model.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# ruff: noqa: F811
22
import json
3+
import re
34
import typing as t
45
from datetime import date, datetime
56
from pathlib import Path
@@ -8449,6 +8450,37 @@ def test_blueprinting_with_quotes(tmp_path: Path) -> None:
84498450
assert t.cast(exp.Query, m2.render_query()).sql() == '''SELECT 'c d' AS "c1", "c d" AS "c2"'''
84508451

84518452

8453+
def test_blueprinting_with_error(tmp_path: Path) -> None:
8454+
init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY)
8455+
8456+
template_with_errors = tmp_path / "models/template_with_errors.sql"
8457+
template_with_errors.parent.mkdir(parents=True, exist_ok=True)
8458+
template_with_errors.write_text(
8459+
"""
8460+
MODEL (
8461+
name m.@{bp_var},
8462+
blueprints (
8463+
(bp_var := "a b"),
8464+
(bp_var := 'c d'),
8465+
),
8466+
);
8467+
8468+
SELECT @bp_var AS c1, @var{bp_var} AS c2
8469+
"""
8470+
)
8471+
8472+
# Validate we display the stack trace as well before the error message
8473+
with pytest.raises(
8474+
ConfigError,
8475+
match=re.compile(
8476+
r"Traceback \(most recent call last\):.*Failed to resolve macros for", re.DOTALL
8477+
),
8478+
):
8479+
ctx = Context(
8480+
config=Config(model_defaults=ModelDefaultsConfig(dialect="duckdb")), paths=tmp_path
8481+
)
8482+
8483+
84528484
def test_blueprint_variable_precedence_sql(tmp_path: Path, assert_exp_eq: t.Callable) -> None:
84538485
init_example_project(tmp_path, dialect="duckdb", template=ProjectTemplate.EMPTY)
84548486

0 commit comments

Comments
 (0)