@@ -55,12 +55,47 @@ def make_python_env(
5555 blueprint_variables = blueprint_variables or {}
5656
5757 used_macros : t .Dict [str , t .Tuple [MacroCallable , bool ]] = {}
58- used_variables = dict .fromkeys (referenced_variables or set (), False ) # var -> is_metadata
58+
59+ # var -> True: var is metadata-only
60+ # var -> False: var is not metadata-only
61+ # var -> None: cannot determine whether var is metadata-only yet, need to walk macros first
62+ used_variables : t .Dict [str , t .Optional [bool ]] = dict .fromkeys (
63+ referenced_variables or set (), False
64+ )
65+
66+ # id(expr) -> true: expr appears under the AST of a metadata-only macro function
67+ # id(expr) -> false: expr appears under the AST of a macro function whose metadata status we don't yet know
68+ expr_under_metadata_macro_func : t .Dict [int , bool ] = {}
5969
6070 # For an expression like @foo(@v1, @bar(@v1, @v2), @v3), the following mapping would be:
6171 # v1 -> {"foo", "bar"}, v2 -> {"bar"}, v3 -> "foo"
6272 macro_funcs_by_used_var : t .DefaultDict [str , t .Set [str ]] = defaultdict (set )
6373
74+ def _is_metadata_var (
75+ name : str , expression : exp .Expression , appears_in_metadata_expression : bool
76+ ) -> t .Optional [bool ]:
77+ is_metadata_so_far = used_variables .get (name , True )
78+ if is_metadata_so_far is False :
79+ return False
80+
81+ appears_under_metadata_macro_func = expr_under_metadata_macro_func .get (id (expression ))
82+ if is_metadata_so_far and (
83+ appears_in_metadata_expression or appears_under_metadata_macro_func
84+ ):
85+ return True
86+
87+ if appears_under_metadata_macro_func is False :
88+ return None
89+
90+ return False
91+
92+ def _is_metadata_macro (name : str , appears_in_metadata_expression : bool ) -> bool :
93+ if name in used_macros :
94+ is_metadata_so_far = used_macros [name ][1 ]
95+ return is_metadata_so_far and appears_in_metadata_expression
96+
97+ return appears_in_metadata_expression
98+
6499 expressions = ensure_list (expressions )
65100 for expression_metadata in expressions :
66101 if isinstance (expression_metadata , tuple ):
@@ -77,11 +112,8 @@ def make_python_env(
77112 if name not in macros :
78113 continue
79114
80- # If this macro has been seen before as a non-metadata macro, prioritize that
81- used_macros [name ] = (
82- macros [name ],
83- used_macros .get (name , (None , is_metadata ))[1 ] and is_metadata ,
84- )
115+ used_macros [name ] = (macros [name ], _is_metadata_macro (name , is_metadata ))
116+
85117 if name in (c .VAR , c .BLUEPRINT_VAR ):
86118 args = macro_func_or_var .this .expressions
87119 if len (args ) < 1 :
@@ -96,20 +128,22 @@ def make_python_env(
96128 )
97129
98130 var_name = args [0 ].this .lower ()
99- used_variables [var_name ] = used_variables .get (var_name , True ) and is_metadata
131+ used_variables [var_name ] = _is_metadata_var (
132+ name , macro_func_or_var , is_metadata
133+ )
100134 else :
101- for var_ref in _extract_macro_func_variable_references (macro_func_or_var ):
135+ var_refs , _expr_under_metadata_macro_func = (
136+ _extract_macro_func_variable_references (macro_func_or_var , is_metadata )
137+ )
138+ expr_under_metadata_macro_func .update (_expr_under_metadata_macro_func )
139+ for var_ref in var_refs :
102140 macro_funcs_by_used_var [var_ref ].add (name )
103141 elif macro_func_or_var .__class__ is d .MacroVar :
104142 name = macro_func_or_var .name .lower ()
105143 if name in macros :
106- # If this macro has been seen before as a non-metadata macro, prioritize that
107- used_macros [name ] = (
108- macros [name ],
109- used_macros .get (name , (None , is_metadata ))[1 ] and is_metadata ,
110- )
144+ used_macros [name ] = (macros [name ], _is_metadata_macro (name , is_metadata ))
111145 elif name in variables or name in blueprint_variables :
112- used_variables [name ] = used_variables . get (name , True ) and is_metadata
146+ used_variables [name ] = _is_metadata_var (name , macro_func_or_var , is_metadata )
113147 elif (
114148 isinstance (macro_func_or_var , (exp .Identifier , d .MacroStrReplace , d .MacroSQL ))
115149 ) and "@" in macro_func_or_var .name :
@@ -118,8 +152,8 @@ def make_python_env(
118152 ):
119153 var_name = braced_identifier or identifier
120154 if var_name in variables or var_name in blueprint_variables :
121- used_variables [var_name ] = (
122- used_variables . get ( var_name , True ) and is_metadata
155+ used_variables [var_name ] = _is_metadata_var (
156+ var_name , macro_func_or_var , is_metadata
123157 )
124158
125159 for macro_ref in jinja_macro_references or set ():
@@ -150,8 +184,12 @@ def make_python_env(
150184 )
151185
152186
153- def _extract_macro_func_variable_references (macro_func : exp .Expression ) -> t .Set [str ]:
187+ def _extract_macro_func_variable_references (
188+ macro_func : exp .Expression ,
189+ is_metadata : bool ,
190+ ) -> t .Tuple [t .Set [str ], t .Dict [int , bool ]]:
154191 references = set ()
192+ expr_under_metadata_macro_func = {}
155193
156194 # Don't descend into nested MacroFunc nodes besides @VAR() and @BLUEPRINT_VAR(), because
157195 # they will be handled in a separate call of _extract_macro_func_variable_references.
@@ -169,20 +207,23 @@ def _prune_nested_macro_func(expression: exp.Expression) -> bool:
169207
170208 if this .name .lower () in (c .VAR , c .BLUEPRINT_VAR ) and args and args [0 ].is_string :
171209 references .add (args [0 ].this .lower ())
210+ expr_under_metadata_macro_func [id (n )] = is_metadata
172211 elif isinstance (n , d .MacroVar ):
173212 references .add (n .name .lower ())
213+ expr_under_metadata_macro_func [id (n )] = is_metadata
174214 elif isinstance (n , (exp .Identifier , d .MacroStrReplace , d .MacroSQL )) and "@" in n .name :
175215 references .update (
176216 (braced_identifier or identifier ).lower ()
177217 for _ , identifier , braced_identifier , _ in MacroStrTemplate .pattern .findall (n .name )
178218 )
219+ expr_under_metadata_macro_func [id (n )] = is_metadata
179220
180- return references
221+ return ( references , expr_under_metadata_macro_func )
181222
182223
183224def _add_variables_to_python_env (
184225 python_env : t .Dict [str , Executable ],
185- used_variables : t .Dict [str , bool ],
226+ used_variables : t .Dict [str , t . Optional [ bool ] ],
186227 variables : t .Optional [t .Dict [str , t .Any ]],
187228 strict_resolution : bool = True ,
188229 blueprint_variables : t .Optional [t .Dict [str , t .Any ]] = None ,
@@ -197,14 +238,18 @@ def _add_variables_to_python_env(
197238 blueprint_variables = blueprint_variables ,
198239 )
199240 for var_name , is_metadata in python_used_variables .items ():
200- used_variables [var_name ] = used_variables .get (var_name , True ) and is_metadata
241+ used_variables [var_name ] = is_metadata and used_variables .get (var_name )
201242
202243 # Variables are treated as metadata when:
203244 # - They are only referenced in metadata-only contexts, such as `audits (...)`, virtual statements, etc
204245 # - They are only referenced in metadata-only macros, either as their arguments or within their definitions
205246 metadata_used_variables = set ()
206247 for used_var , macro_names in (macro_funcs_by_used_var or {}).items ():
207- if used_variables .get (used_var ) or all (
248+ used_var_is_metadata = used_variables .get (used_var )
249+ if used_var_is_metadata is False :
250+ continue
251+
252+ if used_var_is_metadata or all (
208253 name in python_env and python_env [name ].is_metadata for name in macro_names
209254 ):
210255 metadata_used_variables .add (used_var )
0 commit comments