Skip to content

Commit 57e3fe8

Browse files
pr feedback; add unit tests
1 parent 637b80f commit 57e3fe8

File tree

4 files changed

+124
-58
lines changed

4 files changed

+124
-58
lines changed

sqlmesh/core/renderer.py

Lines changed: 4 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,6 @@
55
from contextlib import contextmanager
66
from functools import partial
77
from pathlib import Path
8-
import re
9-
from sys import exc_info
10-
from traceback import walk_tb
11-
from jinja2 import UndefinedError
12-
from jinja2.runtime import Macro
138

149
from sqlglot import exp, parse
1510
from sqlglot.errors import SqlglotError
@@ -35,7 +30,7 @@
3530
SQLMeshError,
3631
raise_config_error,
3732
)
38-
from sqlmesh.utils.jinja import JinjaMacroRegistry
33+
from sqlmesh.utils.jinja import JinjaMacroRegistry, extract_error_details
3934
from sqlmesh.utils.metaprogramming import Executable, prepare_env
4035

4136
if t.TYPE_CHECKING:
@@ -252,43 +247,9 @@ def _resolve_table(table: str | exp.Table) -> str:
252247
except ParsetimeAdapterCallError:
253248
raise
254249
except Exception as ex:
255-
error_details: t.List[str] = []
256-
if isinstance(ex, UndefinedError):
257-
try:
258-
_, _, exc_traceback = exc_info()
259-
for frame, _ in walk_tb(exc_traceback):
260-
if frame.f_code.co_name == "_invoke":
261-
macro = frame.f_locals.get("self")
262-
if isinstance(macro, Macro):
263-
arguments = frame.f_locals.get("arguments", [])
264-
arg_strs = [
265-
f"'{a}'" if isinstance(a, str) else str(a)
266-
for a in arguments
267-
]
268-
error_details.append(
269-
f"\nError when calling jinja macro: {macro.name}({', '.join(arg_strs)})\n"
270-
)
271-
for package in self._jinja_macro_registry.packages:
272-
try:
273-
if macro_info := self._jinja_macro_registry._get_macro(
274-
macro.name, package
275-
):
276-
error_details.append(
277-
"Macro definition:\n" + macro_info.definition
278-
)
279-
break
280-
except:
281-
pass
282-
break
283-
except:
284-
# fall back to the generic error message if frame analysis fails
285-
pass
286-
287-
if match := re.search(r"'(\w+)'", str(ex)):
288-
error_details.append(f"\nUndefined macro/variable: '{match.group(1)}'\n")
289-
290-
error_msg = f"Could not render or parse jinja at '{self._path}'."
291-
error_msg += "\n" + "\n".join(error_details) if error_details else f"\n{ex}"
250+
error_msg = f"Could not render or parse jinja for '{self._path}'.\n" + (
251+
extract_error_details(ex) or f"{ex}"
252+
)
292253

293254
raise ConfigError(error_msg) from ex
294255

sqlmesh/utils/jinja.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77
import zlib
88
from collections import defaultdict
99
from enum import Enum
10+
from sys import exc_info
11+
from traceback import walk_tb
1012

11-
from jinja2 import Environment, Template, nodes
13+
from jinja2 import Environment, Template, nodes, UndefinedError
14+
from jinja2.runtime import Macro
1215
from sqlglot import Dialect, Expression, Parser, TokenType
1316

1417
from sqlmesh.core import constants as c
@@ -664,3 +667,24 @@ def make_jinja_registry(
664667
jinja_registry = jinja_registry.trim(jinja_references)
665668

666669
return jinja_registry
670+
671+
672+
def extract_error_details(ex: Exception) -> str:
673+
"""Extracts a readable message from a Jinja2 error, to include missing name and macro."""
674+
675+
error_details = ""
676+
if isinstance(ex, UndefinedError):
677+
if match := re.search(r"'(\w+)'", str(ex)):
678+
error_details += f"\nUndefined macro/variable: '{match.group(1)}'"
679+
try:
680+
_, _, exc_traceback = exc_info()
681+
for frame, _ in walk_tb(exc_traceback):
682+
if frame.f_code.co_name == "_invoke":
683+
macro = frame.f_locals.get("self")
684+
if isinstance(macro, Macro):
685+
error_details += f" in macro: {macro.name}\n"
686+
break
687+
except:
688+
# to fall back to the generic error message if frame analysis fails
689+
pass
690+
return error_details

tests/core/test_context.py

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -631,6 +631,49 @@ def test_env_and_default_schema_normalization(mocker: MockerFixture):
631631
assert list(context.fetchdf('select c from "DEFAULT__DEV"."X"')["c"])[0] == 1
632632

633633

634+
def test_jinja_macro_undefined_variable_error(tmp_path: pathlib.Path):
635+
models_dir = tmp_path / "models"
636+
models_dir.mkdir(parents=True)
637+
macros_dir = tmp_path / "macros"
638+
macros_dir.mkdir(parents=True)
639+
640+
macro_file = macros_dir / "my_macros.sql"
641+
macro_file.write_text("""
642+
{%- macro generate_select(table_name) -%}
643+
{%- if target.name == 'production' -%}
644+
{%- set results = run_query('SELECT 1') -%}
645+
{%- endif -%}
646+
SELECT {{ results.columns[0].values()[0] }} FROM {{ table_name }}
647+
{%- endmacro -%}
648+
""")
649+
650+
model_file = models_dir / "my_model.sql"
651+
model_file.write_text("""
652+
MODEL (
653+
name my_schema.my_model,
654+
kind FULL
655+
);
656+
657+
JINJA_QUERY_BEGIN;
658+
{{ generate_select('users') }}
659+
JINJA_END;
660+
""")
661+
662+
config_file = tmp_path / "config.yaml"
663+
config_file.write_text("""
664+
model_defaults:
665+
dialect: duckdb
666+
""")
667+
668+
with pytest.raises(ConfigError) as exc_info:
669+
Context(paths=str(tmp_path))
670+
671+
error_message = str(exc_info.value)
672+
assert "Failed to load model" in error_message
673+
assert "Could not render or parse jinja for" in error_message
674+
assert "Undefined macro/variable: 'target' in macro: generate_select" in error_message
675+
676+
634677
def test_clear_caches(tmp_path: pathlib.Path):
635678
models_dir = tmp_path / "models"
636679

@@ -2497,7 +2540,7 @@ def test_plan_min_intervals(tmp_path: Path):
24972540
),
24982541
start '2020-01-01',
24992542
cron '@daily'
2500-
);
2543+
);
25012544
25022545
select @start_ds as start_ds, @end_ds as end_ds, @start_dt as start_dt, @end_dt as end_dt;
25032546
""")
@@ -2510,9 +2553,9 @@ def test_plan_min_intervals(tmp_path: Path):
25102553
),
25112554
start '2020-01-01',
25122555
cron '@weekly'
2513-
);
2556+
);
25142557
2515-
select @start_ds as start_ds, @end_ds as end_ds, @start_dt as start_dt, @end_dt as end_dt;
2558+
select @start_ds as start_ds, @end_ds as end_ds, @start_dt as start_dt, @end_dt as end_dt;
25162559
""")
25172560

25182561
(tmp_path / "models" / "monthly_model.sql").write_text("""
@@ -2523,9 +2566,9 @@ def test_plan_min_intervals(tmp_path: Path):
25232566
),
25242567
start '2020-01-01',
25252568
cron '@monthly'
2526-
);
2569+
);
25272570
2528-
select @start_ds as start_ds, @end_ds as end_ds, @start_dt as start_dt, @end_dt as end_dt;
2571+
select @start_ds as start_ds, @end_ds as end_ds, @start_dt as start_dt, @end_dt as end_dt;
25292572
""")
25302573

25312574
(tmp_path / "models" / "ended_daily_model.sql").write_text("""
@@ -2537,9 +2580,9 @@ def test_plan_min_intervals(tmp_path: Path):
25372580
start '2020-01-01',
25382581
end '2020-01-18',
25392582
cron '@daily'
2540-
);
2583+
);
25412584
2542-
select @start_ds as start_ds, @end_ds as end_ds, @start_dt as start_dt, @end_dt as end_dt;
2585+
select @start_ds as start_ds, @end_ds as end_ds, @start_dt as start_dt, @end_dt as end_dt;
25432586
""")
25442587

25452588
context.load()
@@ -2672,7 +2715,7 @@ def test_plan_min_intervals_adjusted_for_downstream(tmp_path: Path):
26722715
),
26732716
start '2020-01-01',
26742717
cron '@hourly'
2675-
);
2718+
);
26762719
26772720
select @start_dt as start_dt, @end_dt as end_dt;
26782721
""")
@@ -2681,11 +2724,11 @@ def test_plan_min_intervals_adjusted_for_downstream(tmp_path: Path):
26812724
MODEL (
26822725
name sqlmesh_example.two_hourly_model,
26832726
kind INCREMENTAL_BY_TIME_RANGE (
2684-
time_column start_dt
2727+
time_column start_dt
26852728
),
26862729
start '2020-01-01',
26872730
cron '0 */2 * * *'
2688-
);
2731+
);
26892732
26902733
select start_dt, end_dt from sqlmesh_example.hourly_model where start_dt between @start_dt and @end_dt;
26912734
""")
@@ -2694,11 +2737,11 @@ def test_plan_min_intervals_adjusted_for_downstream(tmp_path: Path):
26942737
MODEL (
26952738
name sqlmesh_example.unrelated_monthly_model,
26962739
kind INCREMENTAL_BY_TIME_RANGE (
2697-
time_column start_dt
2740+
time_column start_dt
26982741
),
26992742
start '2020-01-01',
27002743
cron '@monthly'
2701-
);
2744+
);
27022745
27032746
select @start_dt as start_dt, @end_dt as end_dt;
27042747
""")
@@ -2711,7 +2754,7 @@ def test_plan_min_intervals_adjusted_for_downstream(tmp_path: Path):
27112754
),
27122755
start '2020-01-01',
27132756
cron '@daily'
2714-
);
2757+
);
27152758
27162759
select start_dt, end_dt from sqlmesh_example.hourly_model where start_dt between @start_dt and @end_dt;
27172760
""")
@@ -2724,7 +2767,7 @@ def test_plan_min_intervals_adjusted_for_downstream(tmp_path: Path):
27242767
),
27252768
start '2020-01-01',
27262769
cron '@weekly'
2727-
);
2770+
);
27282771
27292772
select start_dt, end_dt from sqlmesh_example.daily_model where start_dt between @start_dt and @end_dt;
27302773
""")

tests/dbt/test_model.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from pathlib import Path
66

77
from sqlglot import exp
8+
from sqlglot.errors import SchemaError
89
from sqlmesh import Context
910
from sqlmesh.core.model import TimeColumn, IncrementalByTimeRangeKind
1011
from sqlmesh.core.model.kind import OnDestructiveChange, OnAdditiveChange
@@ -579,3 +580,40 @@ def test_load_microbatch_with_ref_no_filter(
579580
context.render(microbatch_two_snapshot_fqn, start="2025-01-01", end="2025-01-10").sql()
580581
== 'SELECT "microbatch"."cola" AS "cola", "microbatch"."ds" AS "ds" FROM "local"."main"."microbatch" AS "microbatch"'
581582
)
583+
584+
585+
def test_dbt_jinja_macro_undefined_variable_error(create_empty_project):
586+
project_dir, model_dir = create_empty_project()
587+
588+
macros_dir = project_dir / "macros"
589+
macros_dir.mkdir()
590+
591+
# the execute guard in the macro is so that dbt won't fail on the manifest loading earlier
592+
macro_file = macros_dir / "my_macro.sql"
593+
macro_file.write_text("""
594+
{%- macro select_columns(table_name) -%}
595+
{% if execute %}
596+
{%- if target.name == 'production' -%}
597+
{%- set columns = run_query('SELECT column_name FROM information_schema.columns WHERE table_name = \'' ~ table_name ~ '\'') -%}
598+
{%- endif -%}
599+
SELECT {{ columns.rows[0][0] }} FROM {{ table_name }}
600+
{%- endif -%}
601+
{%- endmacro -%}
602+
""")
603+
604+
model_file = model_dir / "my_model.sql"
605+
model_file.write_text("""
606+
{{ config(
607+
materialized='table'
608+
) }}
609+
610+
{{ select_columns('users') }}
611+
""")
612+
613+
with pytest.raises(SchemaError) as exc_info:
614+
Context(paths=project_dir)
615+
616+
error_message = str(exc_info.value)
617+
assert "Failed to update model schemas" in error_message
618+
assert "Could not render or parse jinja for" in error_message
619+
assert "Undefined macro/variable: 'columns' in macro: select_columns" in error_message

0 commit comments

Comments
 (0)