@@ -9,6 +9,27 @@ use minijinja::{
99use crate :: functions:: DocMacro ;
1010use 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
1334pub 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+ }
0 commit comments