Skip to content

Commit 2512548

Browse files
Yashwanth Reddy MaliYashwanth Reddy Mali
authored andcommitted
fix: use restricted Jinja context for macro property resolution
Match dbt Core by preventing project/package macro execution while rendering macro properties YAML descriptions, while preserving doc() and parse metadata context.
1 parent bc15fc7 commit 2512548

5 files changed

Lines changed: 248 additions & 12 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
kind: Fixes
2+
body: "[dbt-fusion] Use restricted Jinja context for macro property resolution to match dbt Core behavior (#1576)"
3+
time: 2026-04-13T05:48:00.000000+00:00
4+
custom:
5+
author: yxshwanth
6+
issue: "1576"
7+
project: dbt-fusion

crates/dbt-jinja-utils/src/phases/parse/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ pub mod init;
77
pub mod sql_resource;
88

99
pub use crate::utils::render_extract_ref_or_source_expr;
10-
pub use resolve_context::build_resolve_context;
10+
pub use resolve_context::{build_macro_properties_resolve_context, build_resolve_context};
1111
pub use resolve_model_context::build_resolve_model_context;

crates/dbt-jinja-utils/src/phases/parse/resolve_context.rs

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,27 @@ use minijinja::{
99
use crate::functions::DocMacro;
1010
use crate::phases::compile_and_run_context::DbtNamespace;
1111

12+
/// Builds a context for resolving `macros:` entries in YAML property files and for patching
13+
/// macro catalog fields such as `description` in `schema.yml`.
14+
///
15+
/// dbt-core only exposes a narrow Jinja scope there (e.g. `doc()`), not callable project
16+
/// macros. Omitting macro namespace keys matches that behavior so `{{ my_macro(...) }}` in a
17+
/// macro description fails instead of being rendered into SQL at parse time.
18+
pub fn build_macro_properties_resolve_context(
19+
root_project_name: &str,
20+
local_project_name: &str,
21+
docs_macros: &BTreeMap<String, DbtDocsMacro>,
22+
macro_dispatch_order: BTreeMap<String, Vec<String>>,
23+
) -> BTreeMap<String, MinijinjaValue> {
24+
build_resolve_context(
25+
root_project_name,
26+
local_project_name,
27+
docs_macros,
28+
macro_dispatch_order,
29+
vec![],
30+
)
31+
}
32+
1233
/// Builds a context for resolving models
1334
pub fn build_resolve_context(
1435
root_project_name: &str,
@@ -63,3 +84,207 @@ pub fn build_resolve_context(
6384

6485
ctx
6586
}
87+
88+
#[cfg(test)]
89+
mod tests {
90+
use std::collections::BTreeSet;
91+
use std::path::PathBuf;
92+
use std::sync::Mutex;
93+
94+
use super::*;
95+
use crate::environment_builder::{JinjaEnvBuilder, MacroUnitsWrapper};
96+
use crate::serde::into_typed_with_jinja;
97+
use dbt_adapter::sql_types::SATypeOpsImpl;
98+
use dbt_adapter::Adapter;
99+
use dbt_adapter_core::AdapterType;
100+
use dbt_common::io_args::IoArgs;
101+
use dbt_schemas::schemas::macros::DbtDocsMacro;
102+
use dbt_schemas::schemas::properties::MacrosProperties;
103+
use dbt_schemas::schemas::relations::DEFAULT_DBT_QUOTING;
104+
use minijinja::dispatch_object::THREAD_LOCAL_DEPENDENCIES;
105+
use minijinja::macro_unit::{MacroInfo, MacroUnit};
106+
use minijinja::machinery::Span;
107+
use minijinja::UndefinedBehavior;
108+
109+
static DEPS_TEST_LOCK: Mutex<()> = Mutex::new(());
110+
111+
fn set_thread_local_dependencies(pkgs: impl IntoIterator<Item = String>) {
112+
let _guard = DEPS_TEST_LOCK.lock().unwrap();
113+
let deps = THREAD_LOCAL_DEPENDENCIES.get_or_init(|| Mutex::new(BTreeSet::new()));
114+
let mut deps = deps.lock().unwrap();
115+
deps.clear();
116+
deps.extend(pkgs);
117+
}
118+
119+
fn create_macro_unit(name: &str, sql: &str) -> MacroUnit {
120+
MacroUnit {
121+
info: MacroInfo {
122+
name: name.to_string(),
123+
path: PathBuf::from("macros/test.sql"),
124+
span: Span {
125+
start_line: 0,
126+
start_col: 0,
127+
start_offset: 0,
128+
end_line: 0,
129+
end_col: 0,
130+
end_offset: 0,
131+
},
132+
funcsign: None,
133+
args: vec![],
134+
unique_id: "test".to_string(),
135+
name_span: Span::default(),
136+
},
137+
sql: sql.to_string(),
138+
}
139+
}
140+
141+
fn parse_jinja_env_with_my_pkg_macro() -> crate::jinja_environment::JinjaEnv {
142+
set_thread_local_dependencies(std::iter::once("my_pkg".to_string()));
143+
let mut macro_units = MacroUnitsWrapper::new(BTreeMap::new());
144+
macro_units.macros.insert(
145+
"my_pkg".to_string(),
146+
vec![create_macro_unit(
147+
"cents_to_dollars",
148+
"{% macro cents_to_dollars(column_name) %}{{ column_name }} / 100.0{% endmacro %}",
149+
)],
150+
);
151+
let adapter = Adapter::new_parse_phase_adapter(
152+
AdapterType::Postgres,
153+
dbt_yaml::Mapping::default(),
154+
DEFAULT_DBT_QUOTING,
155+
Box::new(SATypeOpsImpl::new(AdapterType::Postgres)),
156+
None,
157+
);
158+
JinjaEnvBuilder::new()
159+
.with_undefined_behavior(UndefinedBehavior::Strict)
160+
.with_adapter(std::sync::Arc::new(adapter))
161+
.with_root_package("my_pkg".to_string())
162+
.try_with_macros(macro_units)
163+
.expect("Failed to register macros")
164+
.build()
165+
}
166+
167+
/// One macro-properties YAML blob; description uses the package namespace (matches Fusion
168+
/// before the fix when namespaces were injected).
169+
fn macro_yaml_value_with_desc(description: &str) -> dbt_yaml::Value {
170+
let mut mapping = dbt_yaml::Mapping::new();
171+
mapping.insert(
172+
dbt_yaml::Value::String("name".to_string(), dbt_yaml::Span::default()),
173+
dbt_yaml::Value::String("cents_to_dollars".to_string(), dbt_yaml::Span::default()),
174+
);
175+
mapping.insert(
176+
dbt_yaml::Value::String("description".to_string(), dbt_yaml::Span::default()),
177+
dbt_yaml::Value::String(description.to_string(), dbt_yaml::Span::default()),
178+
);
179+
dbt_yaml::Value::Mapping(mapping, dbt_yaml::Span::default())
180+
}
181+
182+
#[test]
183+
fn macro_properties_context_omits_macro_namespace_keys() {
184+
let docs = BTreeMap::new();
185+
let dispatch = BTreeMap::new();
186+
let full = build_resolve_context(
187+
"root_pkg",
188+
"local_pkg",
189+
&docs,
190+
dispatch.clone(),
191+
vec!["dbt".to_string(), "local_pkg".to_string()],
192+
);
193+
let narrow = build_macro_properties_resolve_context(
194+
"root_pkg",
195+
"local_pkg",
196+
&docs,
197+
dispatch,
198+
);
199+
assert!(full.contains_key("dbt"));
200+
assert!(full.contains_key("local_pkg"));
201+
assert!(!narrow.contains_key("dbt"));
202+
assert!(!narrow.contains_key("local_pkg"));
203+
assert!(narrow.get("doc").is_some());
204+
assert_eq!(
205+
narrow.get(TARGET_PACKAGE_NAME).and_then(|v| v.as_str()),
206+
Some("local_pkg")
207+
);
208+
}
209+
210+
#[test]
211+
fn narrow_context_errors_on_macro_call_in_description() {
212+
let env = parse_jinja_env_with_my_pkg_macro();
213+
let dispatch = BTreeMap::new();
214+
let full_ctx = build_resolve_context(
215+
"my_pkg",
216+
"my_pkg",
217+
&BTreeMap::new(),
218+
dispatch.clone(),
219+
vec!["my_pkg".to_string()],
220+
);
221+
let narrow_ctx = build_macro_properties_resolve_context("my_pkg", "my_pkg", &BTreeMap::new(), dispatch);
222+
223+
let yml = macro_yaml_value_with_desc("{{ my_pkg.cents_to_dollars('price_cents') }}");
224+
let io = IoArgs::default();
225+
let parsed: MacrosProperties = into_typed_with_jinja(
226+
&io,
227+
yml.clone(),
228+
false,
229+
&env,
230+
&full_ctx,
231+
&[],
232+
None,
233+
false,
234+
)
235+
.expect("full resolve context should render macro in description");
236+
assert!(
237+
parsed.description.unwrap().contains("price_cents / 100.0"),
238+
"expected macro body in description"
239+
);
240+
241+
let err = into_typed_with_jinja::<MacrosProperties, _>(
242+
&io,
243+
yml,
244+
false,
245+
&env,
246+
&narrow_ctx,
247+
&[],
248+
None,
249+
false,
250+
)
251+
.expect_err("narrow context should not resolve package macros in description");
252+
let msg = err.to_string();
253+
assert!(
254+
msg.contains("undefined") || msg.contains("Undefined"),
255+
"expected undefined error, got: {msg}"
256+
);
257+
}
258+
259+
#[test]
260+
fn narrow_context_allows_doc_in_macro_description() {
261+
let env = parse_jinja_env_with_my_pkg_macro();
262+
let mut docs = BTreeMap::new();
263+
docs.insert(
264+
"doc.my_pkg.my_doc".to_string(),
265+
DbtDocsMacro {
266+
name: "my_doc".to_string(),
267+
package_name: "my_pkg".to_string(),
268+
path: PathBuf::from("models/docs.md"),
269+
original_file_path: PathBuf::from("models/docs.md"),
270+
unique_id: "doc.my_pkg.my_doc".to_string(),
271+
block_contents: "hello from doc".to_string(),
272+
},
273+
);
274+
let narrow_ctx = build_macro_properties_resolve_context("my_pkg", "my_pkg", &docs, BTreeMap::new());
275+
let yml = macro_yaml_value_with_desc("{{ doc('my_doc') }}");
276+
let io = IoArgs::default();
277+
let parsed: MacrosProperties = into_typed_with_jinja(
278+
&io,
279+
yml,
280+
false,
281+
&env,
282+
&narrow_ctx,
283+
&[],
284+
None,
285+
false,
286+
)
287+
.expect("doc() should work in macro description with narrow context");
288+
assert_eq!(parsed.description.as_deref(), Some("hello from doc"));
289+
}
290+
}

crates/dbt-parser/src/resolve/resolve_properties.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ impl MinimalProperties {
6060
jinja_env: &JinjaEnv,
6161
properties_path: &Path,
6262
base_ctx: &BTreeMap<String, MinijinjaValue>,
63+
macro_properties_ctx: &BTreeMap<String, MinijinjaValue>,
6364
) -> FsResult<()> {
6465
// TODO: This is a bit repetetive. Can be shortened!
6566
if let Some(models) = other.models {
@@ -545,9 +546,9 @@ impl MinimalProperties {
545546
macro_value.clone(),
546547
false,
547548
jinja_env,
548-
base_ctx,
549+
macro_properties_ctx,
549550
&[],
550-
dependency_package_name_from_ctx(jinja_env, base_ctx),
551+
dependency_package_name_from_ctx(jinja_env, macro_properties_ctx),
551552
true,
552553
)?;
553554
if let Some(existing_macro) = self.macros.get_mut(&macro_props.name) {
@@ -597,6 +598,7 @@ pub fn resolve_minimal_properties(
597598
root_package_name: &str,
598599
jinja_env: &JinjaEnv,
599600
base_ctx: &BTreeMap<String, MinijinjaValue>,
601+
macro_properties_ctx: &BTreeMap<String, MinijinjaValue>,
600602
token: &CancellationToken,
601603
) -> FsResult<MinimalProperties> {
602604
let mut minimal_resolved_properties = MinimalProperties {
@@ -646,6 +648,7 @@ pub fn resolve_minimal_properties(
646648
jinja_env,
647649
properties_path,
648650
base_ctx,
651+
macro_properties_ctx,
649652
)?;
650653

651654
if !minimal_resolved_properties.semantic_layer_spec_is_legacy

crates/dbt-parser/src/resolver.rs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use dbt_jinja_utils::listener::JinjaTypeCheckingEventListenerFactory;
1515
use dbt_jinja_utils::node_resolver::{
1616
NodeResolver, check_for_model_deprecations, resolve_dependencies,
1717
};
18-
use dbt_jinja_utils::phases::parse::build_resolve_context;
18+
use dbt_jinja_utils::phases::parse::{build_macro_properties_resolve_context, build_resolve_context};
1919
use dbt_jinja_utils::phases::parse::init::initialize_parse_jinja_environment;
2020
use dbt_jinja_utils::serde::{into_typed_with_error, into_typed_with_jinja};
2121
use dbt_jinja_utils::utils::dependency_package_name_from_ctx;
@@ -275,25 +275,19 @@ pub async fn resolve(
275275
.iter()
276276
.find(|p| p.dbt_project.name == package_name);
277277
if let Some(package) = package {
278-
let namespace_keys: Vec<String> = jinja_env
279-
.env
280-
.get_macro_namespace_registry()
281-
.map(|r| r.keys().map(|k| k.to_string()).collect())
282-
.unwrap_or_default();
283-
let base_ctx = build_resolve_context(
278+
let macro_properties_ctx = build_macro_properties_resolve_context(
284279
root_project_name,
285280
package.dbt_project.name.as_str(),
286281
&macros.docs_macros,
287282
DISPATCH_CONFIG.get().unwrap().read().unwrap().clone(),
288-
namespace_keys,
289283
);
290284
apply_macro_patches(
291285
&arg.io,
292286
&mut macros.macros,
293287
&macro_properties,
294288
&package_name,
295289
&jinja_env,
296-
&base_ctx,
290+
&macro_properties_ctx,
297291
validate_macro_args,
298292
)?;
299293
}
@@ -568,13 +562,20 @@ pub async fn resolve_inner(
568562
DISPATCH_CONFIG.get().unwrap().read().unwrap().clone(),
569563
namespace_keys,
570564
);
565+
let macro_properties_ctx = build_macro_properties_resolve_context(
566+
root_package_name,
567+
package.dbt_project.name.as_str(),
568+
&macros.docs_macros,
569+
DISPATCH_CONFIG.get().unwrap().read().unwrap().clone(),
570+
);
571571
// Resolve the dbt properties (schema.yml) files
572572
let mut min_properties = resolve_minimal_properties(
573573
arg,
574574
package,
575575
root_package_name,
576576
&jinja_env,
577577
&base_ctx,
578+
&macro_properties_ctx,
578579
token,
579580
)?;
580581

0 commit comments

Comments
 (0)