Skip to content

Commit 22ebb0c

Browse files
authored
Merge branch 'TobikoData:main' into doris
2 parents 2b4ccc8 + 32407f9 commit 22ebb0c

File tree

8 files changed

+202
-33
lines changed

8 files changed

+202
-33
lines changed

sqlmesh/core/renderer.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
SQLMeshError,
3131
raise_config_error,
3232
)
33-
from sqlmesh.utils.jinja import JinjaMacroRegistry
33+
from sqlmesh.utils.jinja import JinjaMacroRegistry, extract_error_details
3434
from sqlmesh.utils.metaprogramming import Executable, prepare_env
3535

3636
if t.TYPE_CHECKING:
@@ -239,18 +239,24 @@ def _resolve_table(table: str | exp.Table) -> str:
239239
logger.debug(
240240
f"Rendered Jinja expression for model '{self._model_fqn}' at '{self._path}': '{rendered_expression}'"
241241
)
242-
if rendered_expression.strip():
243-
expressions = [e for e in parse(rendered_expression, read=self._dialect) if e]
244-
245-
if not expressions:
246-
raise ConfigError(f"Failed to parse an expression:\n{self._expression}")
247242
except ParsetimeAdapterCallError:
248243
raise
249244
except Exception as ex:
250245
raise ConfigError(
251-
f"Could not render or parse jinja at '{self._path}'.\n{ex}"
246+
f"Could not render jinja for '{self._path}'.\n" + extract_error_details(ex)
252247
) from ex
253248

249+
if rendered_expression.strip():
250+
try:
251+
expressions = [e for e in parse(rendered_expression, read=self._dialect) if e]
252+
253+
if not expressions:
254+
raise ConfigError(f"Failed to parse an expression:\n{self._expression}")
255+
except Exception as ex:
256+
raise ConfigError(
257+
f"Could not parse the rendered jinja at '{self._path}'.\n{ex}"
258+
) from ex
259+
254260
if this_model:
255261
render_kwargs["this_model"] = this_model
256262

sqlmesh/dbt/adapter.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,8 @@ def compare_dbr_version(self, major: int, minor: int) -> int:
168168

169169
@property
170170
def graph(self) -> t.Any:
171-
return AttributeDict(
171+
flat_graph = self.jinja_globals.get("flat_graph", None)
172+
return flat_graph or AttributeDict(
172173
{
173174
"exposures": {},
174175
"groups": {},
@@ -276,10 +277,6 @@ def __init__(
276277
**table_mapping,
277278
}
278279

279-
@property
280-
def graph(self) -> t.Any:
281-
return self.jinja_globals.get("flat_graph", super().graph)
282-
283280
def get_relation(
284281
self, database: t.Optional[str], schema: str, identifier: str
285282
) -> t.Optional[BaseRelation]:

sqlmesh/dbt/builtin.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -502,8 +502,14 @@ def create_builtin_globals(
502502
else:
503503
builtin_globals["model"] = AttributeDict(model.copy())
504504

505+
builtin_globals["flags"] = (
506+
Flags(which="run") if engine_adapter is not None else Flags(which="parse")
507+
)
508+
builtin_globals["invocation_args_dict"] = {
509+
k.lower(): v for k, v in builtin_globals["flags"].__dict__.items()
510+
}
511+
505512
if engine_adapter is not None:
506-
builtin_globals["flags"] = Flags(which="run")
507513
adapter: BaseAdapter = RuntimeAdapter(
508514
engine_adapter,
509515
jinja_macros,
@@ -521,7 +527,6 @@ def create_builtin_globals(
521527
project_dialect=project_dialect,
522528
)
523529
else:
524-
builtin_globals["flags"] = Flags(which="parse")
525530
adapter = ParsetimeAdapter(
526531
jinja_macros,
527532
jinja_globals={**builtin_globals, **jinja_globals},

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 or str(ex)

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 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/core/test_model.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8588,6 +8588,23 @@ def test_comments_in_jinja_query():
85888588
model.render_query()
85898589

85908590

8591+
def test_jinja_render_parse_error():
8592+
expressions = d.parse(
8593+
"""
8594+
MODEL (name db.test_model);
8595+
8596+
JINJA_QUERY_BEGIN;
8597+
{{ unknown_macro() }}
8598+
JINJA_END;
8599+
"""
8600+
)
8601+
8602+
model = load_sql_based_model(expressions)
8603+
8604+
with pytest.raises(ConfigError, match=r"Could not render jinja"):
8605+
model.render_query()
8606+
8607+
85918608
def test_jinja_render_debug_logging(caplog):
85928609
"""Test that rendered Jinja expressions are logged for debugging."""
85938610
import logging
@@ -8609,7 +8626,7 @@ def test_jinja_render_debug_logging(caplog):
86098626
model = load_sql_based_model(expressions)
86108627

86118628
# Attempt to render - this should fail due to invalid SQL syntax
8612-
with pytest.raises(ConfigError, match=r"Could not render or parse jinja"):
8629+
with pytest.raises(ConfigError, match=r"Could not parse the rendered jinja"):
86138630
model.render_query()
86148631

86158632
# Check that the rendered Jinja was logged

tests/dbt/test_model.py

Lines changed: 54 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,56 @@ 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+
@pytest.mark.slow
586+
def test_dbt_jinja_macro_undefined_variable_error(create_empty_project):
587+
project_dir, model_dir = create_empty_project()
588+
589+
dbt_profile_config = {
590+
"test": {
591+
"outputs": {
592+
"duckdb": {
593+
"type": "duckdb",
594+
"path": str(project_dir.parent / "dbt_data" / "main.db"),
595+
}
596+
},
597+
"target": "duckdb",
598+
}
599+
}
600+
db_profile_file = project_dir / "profiles.yml"
601+
with open(db_profile_file, "w", encoding="utf-8") as f:
602+
YAML().dump(dbt_profile_config, f)
603+
604+
macros_dir = project_dir / "macros"
605+
macros_dir.mkdir()
606+
607+
# the execute guard in the macro is so that dbt won't fail on the manifest loading earlier
608+
macro_file = macros_dir / "my_macro.sql"
609+
macro_file.write_text("""
610+
{%- macro select_columns(table_name) -%}
611+
{% if execute %}
612+
{%- if target.name == 'production' -%}
613+
{%- set columns = run_query('SELECT column_name FROM information_schema.columns WHERE table_name = \'' ~ table_name ~ '\'') -%}
614+
{%- endif -%}
615+
SELECT {{ columns.rows[0][0] }} FROM {{ table_name }}
616+
{%- endif -%}
617+
{%- endmacro -%}
618+
""")
619+
620+
model_file = model_dir / "my_model.sql"
621+
model_file.write_text("""
622+
{{ config(
623+
materialized='table'
624+
) }}
625+
626+
{{ select_columns('users') }}
627+
""")
628+
629+
with pytest.raises(SchemaError) as exc_info:
630+
Context(paths=project_dir)
631+
632+
error_message = str(exc_info.value)
633+
assert "Failed to update model schemas" in error_message
634+
assert "Could not render jinja for" in error_message
635+
assert "Undefined macro/variable: 'columns' in macro: 'select_columns'" in error_message

0 commit comments

Comments
 (0)