@@ -55,12 +55,47 @@ def make_python_env(
55
55
blueprint_variables = blueprint_variables or {}
56
56
57
57
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 ] = {}
59
69
60
70
# For an expression like @foo(@v1, @bar(@v1, @v2), @v3), the following mapping would be:
61
71
# v1 -> {"foo", "bar"}, v2 -> {"bar"}, v3 -> "foo"
62
72
macro_funcs_by_used_var : t .DefaultDict [str , t .Set [str ]] = defaultdict (set )
63
73
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
+
64
99
expressions = ensure_list (expressions )
65
100
for expression_metadata in expressions :
66
101
if isinstance (expression_metadata , tuple ):
@@ -77,11 +112,8 @@ def make_python_env(
77
112
if name not in macros :
78
113
continue
79
114
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
+
85
117
if name in (c .VAR , c .BLUEPRINT_VAR ):
86
118
args = macro_func_or_var .this .expressions
87
119
if len (args ) < 1 :
@@ -96,20 +128,22 @@ def make_python_env(
96
128
)
97
129
98
130
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
+ )
100
134
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 :
102
140
macro_funcs_by_used_var [var_ref ].add (name )
103
141
elif macro_func_or_var .__class__ is d .MacroVar :
104
142
name = macro_func_or_var .name .lower ()
105
143
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 ))
111
145
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 )
113
147
elif (
114
148
isinstance (macro_func_or_var , (exp .Identifier , d .MacroStrReplace , d .MacroSQL ))
115
149
) and "@" in macro_func_or_var .name :
@@ -118,8 +152,8 @@ def make_python_env(
118
152
):
119
153
var_name = braced_identifier or identifier
120
154
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
123
157
)
124
158
125
159
for macro_ref in jinja_macro_references or set ():
@@ -150,8 +184,12 @@ def make_python_env(
150
184
)
151
185
152
186
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 ]]:
154
191
references = set ()
192
+ expr_under_metadata_macro_func = {}
155
193
156
194
# Don't descend into nested MacroFunc nodes besides @VAR() and @BLUEPRINT_VAR(), because
157
195
# 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:
169
207
170
208
if this .name .lower () in (c .VAR , c .BLUEPRINT_VAR ) and args and args [0 ].is_string :
171
209
references .add (args [0 ].this .lower ())
210
+ expr_under_metadata_macro_func [id (n )] = is_metadata
172
211
elif isinstance (n , d .MacroVar ):
173
212
references .add (n .name .lower ())
213
+ expr_under_metadata_macro_func [id (n )] = is_metadata
174
214
elif isinstance (n , (exp .Identifier , d .MacroStrReplace , d .MacroSQL )) and "@" in n .name :
175
215
references .update (
176
216
(braced_identifier or identifier ).lower ()
177
217
for _ , identifier , braced_identifier , _ in MacroStrTemplate .pattern .findall (n .name )
178
218
)
219
+ expr_under_metadata_macro_func [id (n )] = is_metadata
179
220
180
- return references
221
+ return ( references , expr_under_metadata_macro_func )
181
222
182
223
183
224
def _add_variables_to_python_env (
184
225
python_env : t .Dict [str , Executable ],
185
- used_variables : t .Dict [str , bool ],
226
+ used_variables : t .Dict [str , t . Optional [ bool ] ],
186
227
variables : t .Optional [t .Dict [str , t .Any ]],
187
228
strict_resolution : bool = True ,
188
229
blueprint_variables : t .Optional [t .Dict [str , t .Any ]] = None ,
@@ -197,14 +238,18 @@ def _add_variables_to_python_env(
197
238
blueprint_variables = blueprint_variables ,
198
239
)
199
240
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 )
201
242
202
243
# Variables are treated as metadata when:
203
244
# - They are only referenced in metadata-only contexts, such as `audits (...)`, virtual statements, etc
204
245
# - They are only referenced in metadata-only macros, either as their arguments or within their definitions
205
246
metadata_used_variables = set ()
206
247
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 (
208
253
name in python_env and python_env [name ].is_metadata for name in macro_names
209
254
):
210
255
metadata_used_variables .add (used_var )
0 commit comments