sqlmesh 0.209.1.dev12__py3-none-any.whl → 0.209.1.dev13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sqlmesh/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.209.1.dev12'
32
- __version_tuple__ = version_tuple = (0, 209, 1, 'dev12')
31
+ __version__ = version = '0.209.1.dev13'
32
+ __version_tuple__ = version_tuple = (0, 209, 1, 'dev13')
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -438,13 +438,15 @@ def load_audit(
438
438
 
439
439
  extra_kwargs: t.Dict[str, t.Any] = {}
440
440
  if is_standalone:
441
- jinja_macro_refrences, used_variables = extract_macro_references_and_variables(
441
+ jinja_macro_refrences, referenced_variables = extract_macro_references_and_variables(
442
442
  *(gen(s) for s in statements),
443
443
  gen(query),
444
444
  )
445
445
  jinja_macros = (jinja_macros or JinjaMacroRegistry()).trim(jinja_macro_refrences)
446
446
  for jinja_macro in jinja_macros.root_macros.values():
447
- used_variables.update(extract_macro_references_and_variables(jinja_macro.definition)[1])
447
+ referenced_variables.update(
448
+ extract_macro_references_and_variables(jinja_macro.definition)[1]
449
+ )
448
450
 
449
451
  extra_kwargs["jinja_macros"] = jinja_macros
450
452
  extra_kwargs["python_env"] = make_python_env(
@@ -453,7 +455,7 @@ def load_audit(
453
455
  module_path,
454
456
  macros or macro.get_registry(),
455
457
  variables=variables,
456
- used_variables=used_variables,
458
+ referenced_variables=referenced_variables,
457
459
  )
458
460
  extra_kwargs["default_catalog"] = default_catalog
459
461
  if project is not None:
sqlmesh/core/constants.py CHANGED
@@ -80,7 +80,9 @@ REQUIREMENTS = "sqlmesh-requirements.lock"
80
80
  DEFAULT_SCHEMA = "default"
81
81
 
82
82
  SQLMESH_VARS = "__sqlmesh__vars__"
83
+ SQLMESH_VARS_METADATA = "__sqlmesh__vars__metadata__"
83
84
  SQLMESH_BLUEPRINT_VARS = "__sqlmesh__blueprint__vars__"
85
+ SQLMESH_BLUEPRINT_VARS_METADATA = "__sqlmesh__blueprint__vars__metadata__"
84
86
 
85
87
  VAR = "var"
86
88
  BLUEPRINT_VAR = "blueprint_var"
sqlmesh/core/macros.py CHANGED
@@ -210,7 +210,12 @@ class MacroEvaluator:
210
210
  self.macros[normalize_macro_name(k)] = self.env[k]
211
211
  elif v.is_value:
212
212
  value = self.env[k]
213
- if k in (c.SQLMESH_VARS, c.SQLMESH_BLUEPRINT_VARS):
213
+ if k in (
214
+ c.SQLMESH_VARS,
215
+ c.SQLMESH_VARS_METADATA,
216
+ c.SQLMESH_BLUEPRINT_VARS,
217
+ c.SQLMESH_BLUEPRINT_VARS_METADATA,
218
+ ):
214
219
  value = {
215
220
  var_name: (
216
221
  self.parse_one(var_value.sql)
@@ -557,17 +562,25 @@ class MacroEvaluator:
557
562
 
558
563
  def var(self, var_name: str, default: t.Optional[t.Any] = None) -> t.Optional[t.Any]:
559
564
  """Returns the value of the specified variable, or the default value if it doesn't exist."""
560
- return (self.locals.get(c.SQLMESH_VARS) or {}).get(var_name.lower(), default)
565
+ return {
566
+ **(self.locals.get(c.SQLMESH_VARS) or {}),
567
+ **(self.locals.get(c.SQLMESH_VARS_METADATA) or {}),
568
+ }.get(var_name.lower(), default)
561
569
 
562
570
  def blueprint_var(self, var_name: str, default: t.Optional[t.Any] = None) -> t.Optional[t.Any]:
563
571
  """Returns the value of the specified blueprint variable, or the default value if it doesn't exist."""
564
- return (self.locals.get(c.SQLMESH_BLUEPRINT_VARS) or {}).get(var_name.lower(), default)
572
+ return {
573
+ **(self.locals.get(c.SQLMESH_BLUEPRINT_VARS) or {}),
574
+ **(self.locals.get(c.SQLMESH_BLUEPRINT_VARS_METADATA) or {}),
575
+ }.get(var_name.lower(), default)
565
576
 
566
577
  @property
567
578
  def variables(self) -> t.Dict[str, t.Any]:
568
579
  return {
569
580
  **self.locals.get(c.SQLMESH_VARS, {}),
581
+ **self.locals.get(c.SQLMESH_VARS_METADATA, {}),
570
582
  **self.locals.get(c.SQLMESH_BLUEPRINT_VARS, {}),
583
+ **self.locals.get(c.SQLMESH_BLUEPRINT_VARS_METADATA, {}),
571
584
  }
572
585
 
573
586
  def _coerce(self, expr: exp.Expression, typ: t.Any, strict: bool = False) -> t.Any:
@@ -28,7 +28,7 @@ if t.TYPE_CHECKING:
28
28
  from sqlmesh.utils import registry_decorator
29
29
  from sqlmesh.utils.jinja import MacroReference
30
30
 
31
- MacroCallable = registry_decorator
31
+ MacroCallable = t.Union[Executable, registry_decorator]
32
32
 
33
33
 
34
34
  def make_python_env(
@@ -40,7 +40,7 @@ def make_python_env(
40
40
  module_path: Path,
41
41
  macros: MacroRegistry,
42
42
  variables: t.Optional[t.Dict[str, t.Any]] = None,
43
- used_variables: t.Optional[t.Set[str]] = None,
43
+ referenced_variables: t.Optional[t.Set[str]] = None,
44
44
  path: t.Optional[Path] = None,
45
45
  python_env: t.Optional[t.Dict[str, Executable]] = None,
46
46
  strict_resolution: bool = True,
@@ -48,20 +48,64 @@ def make_python_env(
48
48
  dialect: DialectType = None,
49
49
  ) -> t.Dict[str, Executable]:
50
50
  python_env = {} if python_env is None else python_env
51
- variables = variables or {}
52
51
  env: t.Dict[str, t.Tuple[t.Any, t.Optional[bool]]] = {}
53
- used_macros: t.Dict[
54
- str,
55
- t.Tuple[t.Union[Executable | MacroCallable], t.Optional[bool]],
56
- ] = {}
57
- used_variables = (used_variables or set()).copy()
52
+
53
+ variables = variables or {}
54
+ blueprint_variables = blueprint_variables or {}
55
+
56
+ used_macros: t.Dict[str, t.Tuple[MacroCallable, bool]] = {}
57
+
58
+ # var -> True: var is metadata-only
59
+ # var -> False: var is not metadata-only
60
+ # var -> None: cannot determine whether var is metadata-only yet, need to walk macros first
61
+ used_variables: t.Dict[str, t.Optional[bool]] = dict.fromkeys(
62
+ referenced_variables or set(), False
63
+ )
64
+
65
+ # id(expr) -> true: expr appears under the AST of a metadata-only macro function
66
+ # id(expr) -> false: expr appears under the AST of a macro function whose metadata status we don't yet know
67
+ expr_under_metadata_macro_func: t.Dict[int, bool] = {}
68
+
69
+ # For @m1(@m2(@x), @y), we'd get x -> m1 and y -> m1
70
+ outermost_macro_func_ancestor_by_var: t.Dict[str, str] = {}
71
+ visited_macro_funcs: t.Set[int] = set()
72
+
73
+ def _is_metadata_var(
74
+ name: str, expression: exp.Expression, appears_in_metadata_expression: bool
75
+ ) -> t.Optional[bool]:
76
+ is_metadata_so_far = used_variables.get(name, True)
77
+ if is_metadata_so_far is False:
78
+ # We've concluded this variable is definitely not metadata-only
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
+ # The variable appears in a metadata expression, e.g., audits (...),
86
+ # or in the AST of metadata-only macro call, e.g., @FOO(@x)
87
+ return True
88
+
89
+ # The variable appears in the AST of a macro call, but we don't know if it's metadata-only
90
+ if appears_under_metadata_macro_func is False:
91
+ return None
92
+
93
+ # The variable appears elsewhere, e.g., in the model's query: SELECT @x
94
+ return False
95
+
96
+ def _is_metadata_macro(name: str, appears_in_metadata_expression: bool) -> bool:
97
+ if name in used_macros:
98
+ is_metadata_so_far = used_macros[name][1]
99
+ return is_metadata_so_far and appears_in_metadata_expression
100
+
101
+ return appears_in_metadata_expression
58
102
 
59
103
  expressions = ensure_list(expressions)
60
104
  for expression_metadata in expressions:
61
105
  if isinstance(expression_metadata, tuple):
62
106
  expression, is_metadata = expression_metadata
63
107
  else:
64
- expression, is_metadata = expression_metadata, None
108
+ expression, is_metadata = expression_metadata, False
65
109
 
66
110
  if isinstance(expression, d.Jinja):
67
111
  continue
@@ -72,31 +116,51 @@ def make_python_env(
72
116
  if name not in macros:
73
117
  continue
74
118
 
75
- # If this macro has been seen before as a non-metadata macro, prioritize that
76
- used_macros[name] = (
77
- macros[name],
78
- used_macros.get(name, (None, is_metadata))[1] and is_metadata,
79
- )
80
- if name == c.VAR:
119
+ used_macros[name] = (macros[name], _is_metadata_macro(name, is_metadata))
120
+
121
+ if name in (c.VAR, c.BLUEPRINT_VAR):
81
122
  args = macro_func_or_var.this.expressions
82
123
  if len(args) < 1:
83
- raise_config_error("Macro VAR requires at least one argument", path)
124
+ raise_config_error(
125
+ f"Macro {name.upper()} requires at least one argument", path
126
+ )
127
+
84
128
  if not args[0].is_string:
85
129
  raise_config_error(
86
130
  f"The variable name must be a string literal, '{args[0].sql()}' was given instead",
87
131
  path,
88
132
  )
89
- used_variables.add(args[0].this.lower())
133
+
134
+ var_name = args[0].this.lower()
135
+ used_variables[var_name] = _is_metadata_var(
136
+ var_name, macro_func_or_var, is_metadata
137
+ )
138
+ elif id(macro_func_or_var) not in visited_macro_funcs:
139
+ # We only care about the top-level macro function calls to determine the metadata
140
+ # status of the variables referenced in their ASTs. For example, in @m1(@m2(@x)),
141
+ # if m1 is metadata-only but m2 is not, we can still determine that @x only affects
142
+ # the metadata hash, since m2's result feeds into a metadata-only macro function.
143
+ #
144
+ # Generally, if the top-level call is known to be metadata-only or appear in a
145
+ # metadata expression, then we can avoid traversing nested macro function calls.
146
+
147
+ var_refs, _expr_under_metadata_macro_func, _visited_macro_funcs = (
148
+ _extract_macro_func_variable_references(macro_func_or_var, is_metadata)
149
+ )
150
+ expr_under_metadata_macro_func.update(_expr_under_metadata_macro_func)
151
+ visited_macro_funcs.update(_visited_macro_funcs)
152
+ outermost_macro_func_ancestor_by_var |= {var_ref: name for var_ref in var_refs}
90
153
  elif macro_func_or_var.__class__ is d.MacroVar:
91
- name = macro_func_or_var.name.lower()
92
- if name in macros:
93
- # If this macro has been seen before as a non-metadata macro, prioritize that
94
- used_macros[name] = (
95
- macros[name],
96
- used_macros.get(name, (None, is_metadata))[1] and is_metadata,
154
+ var_name = macro_func_or_var.name.lower()
155
+ if var_name in macros:
156
+ used_macros[var_name] = (
157
+ macros[var_name],
158
+ _is_metadata_macro(var_name, is_metadata),
159
+ )
160
+ elif var_name in variables or var_name in blueprint_variables:
161
+ used_variables[var_name] = _is_metadata_var(
162
+ var_name, macro_func_or_var, is_metadata
97
163
  )
98
- elif name in variables:
99
- used_variables.add(name)
100
164
  elif (
101
165
  isinstance(macro_func_or_var, (exp.Identifier, d.MacroStrReplace, d.MacroSQL))
102
166
  ) and "@" in macro_func_or_var.name:
@@ -104,12 +168,14 @@ def make_python_env(
104
168
  macro_func_or_var.name
105
169
  ):
106
170
  var_name = braced_identifier or identifier
107
- if var_name in variables:
108
- used_variables.add(var_name)
171
+ if var_name in variables or var_name in blueprint_variables:
172
+ used_variables[var_name] = _is_metadata_var(
173
+ var_name, macro_func_or_var, is_metadata
174
+ )
109
175
 
110
176
  for macro_ref in jinja_macro_references or set():
111
177
  if macro_ref.package is None and macro_ref.name in macros:
112
- used_macros[macro_ref.name] = (macros[macro_ref.name], None)
178
+ used_macros[macro_ref.name] = (macros[macro_ref.name], False)
113
179
 
114
180
  for name, (used_macro, is_metadata) in used_macros.items():
115
181
  if isinstance(used_macro, Executable):
@@ -131,16 +197,49 @@ def make_python_env(
131
197
  blueprint_variables=blueprint_variables,
132
198
  dialect=dialect,
133
199
  strict_resolution=strict_resolution,
200
+ outermost_macro_func_ancestor_by_var=outermost_macro_func_ancestor_by_var,
134
201
  )
135
202
 
136
203
 
204
+ def _extract_macro_func_variable_references(
205
+ macro_func: exp.Expression,
206
+ is_metadata: bool,
207
+ ) -> t.Tuple[t.Set[str], t.Dict[int, bool], t.Set[int]]:
208
+ var_references = set()
209
+ visited_macro_funcs = set()
210
+ expr_under_metadata_macro_func = {}
211
+
212
+ for n in macro_func.walk():
213
+ if type(n) is d.MacroFunc:
214
+ visited_macro_funcs.add(id(n))
215
+
216
+ this = n.this
217
+ args = this.expressions
218
+
219
+ if this.name.lower() in (c.VAR, c.BLUEPRINT_VAR) and args and args[0].is_string:
220
+ var_references.add(args[0].this.lower())
221
+ expr_under_metadata_macro_func[id(n)] = is_metadata
222
+ elif isinstance(n, d.MacroVar):
223
+ var_references.add(n.name.lower())
224
+ expr_under_metadata_macro_func[id(n)] = is_metadata
225
+ elif isinstance(n, (exp.Identifier, d.MacroStrReplace, d.MacroSQL)) and "@" in n.name:
226
+ var_references.update(
227
+ (braced_identifier or identifier).lower()
228
+ for _, identifier, braced_identifier, _ in MacroStrTemplate.pattern.findall(n.name)
229
+ )
230
+ expr_under_metadata_macro_func[id(n)] = is_metadata
231
+
232
+ return (var_references, expr_under_metadata_macro_func, visited_macro_funcs)
233
+
234
+
137
235
  def _add_variables_to_python_env(
138
236
  python_env: t.Dict[str, Executable],
139
- used_variables: t.Optional[t.Set[str]],
237
+ used_variables: t.Dict[str, t.Optional[bool]],
140
238
  variables: t.Optional[t.Dict[str, t.Any]],
141
239
  strict_resolution: bool = True,
142
240
  blueprint_variables: t.Optional[t.Dict[str, t.Any]] = None,
143
241
  dialect: DialectType = None,
242
+ outermost_macro_func_ancestor_by_var: t.Optional[t.Dict[str, str]] = None,
144
243
  ) -> t.Dict[str, Executable]:
145
244
  _, python_used_variables = parse_dependencies(
146
245
  python_env,
@@ -149,20 +248,67 @@ def _add_variables_to_python_env(
149
248
  variables=variables,
150
249
  blueprint_variables=blueprint_variables,
151
250
  )
152
- used_variables = (used_variables or set()) | python_used_variables
251
+ for var_name, is_metadata in python_used_variables.items():
252
+ used_variables[var_name] = is_metadata and used_variables.get(var_name, True)
253
+
254
+ # Variables are treated as metadata-only when all of their references either:
255
+ # - appear in metadata-only expressions, such as `audits (...)`, virtual statements, etc
256
+ # - appear in the ASTs or definitions of metadata-only macros
257
+ #
258
+ # See also: https://github.com/TobikoData/sqlmesh/pull/4936#issuecomment-3136339936,
259
+ # specifically the "Terminology" and "Observations" section.
260
+ metadata_used_variables = {
261
+ var_name for var_name, is_metadata in used_variables.items() if is_metadata
262
+ }
263
+ for used_var, outermost_macro_func in (outermost_macro_func_ancestor_by_var or {}).items():
264
+ used_var_is_metadata = used_variables.get(used_var)
265
+ if used_var_is_metadata is False:
266
+ continue
267
+
268
+ # At this point we can decide whether a variable reference in a macro call's AST is
269
+ # metadata-only, because we've annotated the corresponding macro call in the python env.
270
+ if outermost_macro_func in python_env and python_env[outermost_macro_func].is_metadata:
271
+ metadata_used_variables.add(used_var)
272
+
273
+ non_metadata_used_variables = set(used_variables) - metadata_used_variables
274
+
275
+ if overlapping_variables := (non_metadata_used_variables & metadata_used_variables):
276
+ raise ConfigError(
277
+ f"Variables {', '.join(overlapping_variables)} are both metadata and non-metadata, "
278
+ "which is unexpected. Please file an issue at https://github.com/TobikoData/sqlmesh/issues/new."
279
+ )
280
+
281
+ metadata_variables = {
282
+ k: v for k, v in (variables or {}).items() if k in metadata_used_variables
283
+ }
284
+ variables = {k: v for k, v in (variables or {}).items() if k in non_metadata_used_variables}
153
285
 
154
- variables = {k: v for k, v in (variables or {}).items() if k in used_variables}
155
286
  if variables:
156
287
  python_env[c.SQLMESH_VARS] = Executable.value(variables, sort_root_dict=True)
288
+ if metadata_variables:
289
+ python_env[c.SQLMESH_VARS_METADATA] = Executable.value(
290
+ metadata_variables, sort_root_dict=True, is_metadata=True
291
+ )
157
292
 
158
293
  if blueprint_variables:
294
+ metadata_blueprint_variables = {
295
+ k: SqlValue(sql=v.sql(dialect=dialect)) if isinstance(v, exp.Expression) else v
296
+ for k, v in blueprint_variables.items()
297
+ if k in metadata_used_variables
298
+ }
159
299
  blueprint_variables = {
160
300
  k.lower(): SqlValue(sql=v.sql(dialect=dialect)) if isinstance(v, exp.Expression) else v
161
301
  for k, v in blueprint_variables.items()
302
+ if k in non_metadata_used_variables
162
303
  }
163
- python_env[c.SQLMESH_BLUEPRINT_VARS] = Executable.value(
164
- blueprint_variables, sort_root_dict=True
165
- )
304
+ if blueprint_variables:
305
+ python_env[c.SQLMESH_BLUEPRINT_VARS] = Executable.value(
306
+ blueprint_variables, sort_root_dict=True
307
+ )
308
+ if metadata_blueprint_variables:
309
+ python_env[c.SQLMESH_BLUEPRINT_VARS_METADATA] = Executable.value(
310
+ metadata_blueprint_variables, sort_root_dict=True, is_metadata=True
311
+ )
166
312
 
167
313
  return python_env
168
314
 
@@ -173,7 +319,7 @@ def parse_dependencies(
173
319
  strict_resolution: bool = True,
174
320
  variables: t.Optional[t.Dict[str, t.Any]] = None,
175
321
  blueprint_variables: t.Optional[t.Dict[str, t.Any]] = None,
176
- ) -> t.Tuple[t.Set[str], t.Set[str]]:
322
+ ) -> t.Tuple[t.Set[str], t.Dict[str, bool]]:
177
323
  """
178
324
  Parses the source of a model function and finds upstream table dependencies
179
325
  and referenced variables based on calls to context / evaluator.
@@ -187,7 +333,8 @@ def parse_dependencies(
187
333
  blueprint_variables: The blueprint variables available to the python environment.
188
334
 
189
335
  Returns:
190
- A tuple containing the set of upstream table dependencies and the set of referenced variables.
336
+ A tuple containing the set of upstream table dependencies and a mapping of
337
+ the referenced variables associated with their metadata status.
191
338
  """
192
339
 
193
340
  class VariableResolutionContext:
@@ -205,12 +352,16 @@ def parse_dependencies(
205
352
  local_env = dict.fromkeys(("context", "evaluator"), VariableResolutionContext)
206
353
 
207
354
  depends_on = set()
208
- used_variables = set()
355
+ used_variables: t.Dict[str, bool] = {}
209
356
 
210
357
  for executable in python_env.values():
211
358
  if not executable.is_definition:
212
359
  continue
360
+
361
+ is_metadata = executable.is_metadata
213
362
  for node in ast.walk(ast.parse(executable.payload)):
363
+ next_variables = set()
364
+
214
365
  if isinstance(node, ast.Call):
215
366
  func = node.func
216
367
  if not isinstance(func, ast.Attribute) or not isinstance(func.value, ast.Name):
@@ -241,8 +392,11 @@ def parse_dependencies(
241
392
 
242
393
  if func.value.id == "context" and func.attr in ("table", "resolve_table"):
243
394
  depends_on.add(get_first_arg("model_name"))
244
- elif func.value.id in ("context", "evaluator") and func.attr == c.VAR:
245
- used_variables.add(get_first_arg("var_name").lower())
395
+ elif func.value.id in ("context", "evaluator") and func.attr in (
396
+ c.VAR,
397
+ c.BLUEPRINT_VAR,
398
+ ):
399
+ next_variables.add(get_first_arg("var_name").lower())
246
400
  elif (
247
401
  isinstance(node, ast.Attribute)
248
402
  and isinstance(node.value, ast.Name)
@@ -250,9 +404,9 @@ def parse_dependencies(
250
404
  and node.attr == c.GATEWAY
251
405
  ):
252
406
  # Check whether the gateway attribute is referenced.
253
- used_variables.add(c.GATEWAY)
407
+ next_variables.add(c.GATEWAY)
254
408
  elif isinstance(node, ast.FunctionDef) and node.name == entrypoint:
255
- used_variables.update(
409
+ next_variables.update(
256
410
  [
257
411
  arg.arg
258
412
  for arg in [*node.args.args, *node.args.kwonlyargs]
@@ -260,6 +414,9 @@ def parse_dependencies(
260
414
  ]
261
415
  )
262
416
 
417
+ for var_name in next_variables:
418
+ used_variables[var_name] = used_variables.get(var_name, True) and bool(is_metadata)
419
+
263
420
  return depends_on, used_variables
264
421
 
265
422
 
@@ -1782,12 +1782,17 @@ class PythonModel(_Model):
1782
1782
  start, end = make_inclusive(start or c.EPOCH, end or c.EPOCH, self.dialect)
1783
1783
  execution_time = to_datetime(execution_time or c.EPOCH)
1784
1784
 
1785
- variables = env.get(c.SQLMESH_VARS, {})
1786
- variables.update(kwargs.pop("variables", {}))
1787
-
1785
+ variables = {
1786
+ **env.get(c.SQLMESH_VARS, {}),
1787
+ **env.get(c.SQLMESH_VARS_METADATA, {}),
1788
+ **kwargs.pop("variables", {}),
1789
+ }
1788
1790
  blueprint_variables = {
1789
1791
  k: d.parse_one(v.sql, dialect=self.dialect) if isinstance(v, SqlValue) else v
1790
- for k, v in env.get(c.SQLMESH_BLUEPRINT_VARS, {}).items()
1792
+ for k, v in {
1793
+ **env.get(c.SQLMESH_BLUEPRINT_VARS, {}),
1794
+ **env.get(c.SQLMESH_BLUEPRINT_VARS_METADATA, {}),
1795
+ }.items()
1791
1796
  }
1792
1797
  try:
1793
1798
  kwargs = {
@@ -1909,11 +1914,11 @@ def _extract_blueprint_variables(blueprint: t.Any, path: Path) -> t.Dict[str, t.
1909
1914
  return {}
1910
1915
  if isinstance(blueprint, (exp.Paren, exp.PropertyEQ)):
1911
1916
  blueprint = blueprint.unnest()
1912
- return {blueprint.left.name: blueprint.right}
1917
+ return {blueprint.left.name.lower(): blueprint.right}
1913
1918
  if isinstance(blueprint, (exp.Tuple, exp.Array)):
1914
- return {e.left.name: e.right for e in blueprint.expressions}
1919
+ return {e.left.name.lower(): e.right for e in blueprint.expressions}
1915
1920
  if isinstance(blueprint, dict):
1916
- return blueprint
1921
+ return {k.lower(): v for k, v in blueprint.items()}
1917
1922
 
1918
1923
  raise_config_error(
1919
1924
  f"Expected a key-value mapping for the blueprint value, got '{blueprint}' instead",
@@ -2509,7 +2514,7 @@ def _create_model(
2509
2514
  if isinstance(getattr(kwargs.get("kind"), "merge_filter", None), exp.Expression):
2510
2515
  statements.append(kwargs["kind"].merge_filter)
2511
2516
 
2512
- jinja_macro_references, used_variables = extract_macro_references_and_variables(
2517
+ jinja_macro_references, referenced_variables = extract_macro_references_and_variables(
2513
2518
  *(gen(e if isinstance(e, exp.Expression) else e[0]) for e in statements)
2514
2519
  )
2515
2520
 
@@ -2532,11 +2537,13 @@ def _create_model(
2532
2537
  _extract_migrated_dbt_variable_references(jinja_macros, variables)
2533
2538
  )
2534
2539
 
2535
- used_variables.update(nested_macro_used_variables)
2540
+ referenced_variables.update(nested_macro_used_variables)
2536
2541
  variables.update(flattened_package_variables)
2537
2542
  else:
2538
2543
  for jinja_macro in jinja_macros.root_macros.values():
2539
- used_variables.update(extract_macro_references_and_variables(jinja_macro.definition)[1])
2544
+ referenced_variables.update(
2545
+ extract_macro_references_and_variables(jinja_macro.definition)[1]
2546
+ )
2540
2547
 
2541
2548
  # Merge model-specific audits with default audits
2542
2549
  if default_audits := defaults.pop("audits", None):
@@ -2598,7 +2605,7 @@ def _create_model(
2598
2605
  module_path,
2599
2606
  macros or macro.get_registry(),
2600
2607
  variables=variables,
2601
- used_variables=used_variables,
2608
+ referenced_variables=referenced_variables,
2602
2609
  path=path,
2603
2610
  python_env=python_env,
2604
2611
  strict_resolution=depends_on is None,
@@ -0,0 +1,74 @@
1
+ """
2
+ This script's goal is to warn users about two situations that could lead to a diff:
3
+
4
+ - They have blueprint models and some of their variables may be trimmed from `python_env`
5
+ - Variables are used in metadata-only contexts, e.g., within metadata-only macros
6
+
7
+ Context:
8
+
9
+ We used to store *all* blueprint variables in `python_env`, even though some of them were
10
+ redundant. For example, if a blueprint variable is only used in the model's `name` property,
11
+ then it is rendered once, at load time, and after that point it's not needed elsewhere.
12
+
13
+ This behavior is now different: we only store the blueprint variables that are required to render
14
+ expressions at runtime, such as model query or runtime-rendered properties, like `merge_filter`.
15
+
16
+ Additionally, variables were previously treated as non-metadata, regardless of how they were used.
17
+ This behavior changed as well: SQLMesh now analyzes variable references and tracks the data flow,
18
+ in order to detect whether changing them will result in a metadata diff for a given model.
19
+
20
+ Some examples where variables can be treated as metadata-only `python_env` executables are:
21
+
22
+ - A variable is referenced in metadata-only macros
23
+ - A variable is referenced in metadata-only expressions, such as virtual update statements
24
+ - A variable is passed as argument to metadata-only macros
25
+ """
26
+
27
+ import json
28
+
29
+ from sqlglot import exp
30
+
31
+ from sqlmesh.core.console import get_console
32
+
33
+ SQLMESH_VARS = "__sqlmesh__vars__"
34
+ SQLMESH_BLUEPRINT_VARS = "__sqlmesh__blueprint__vars__"
35
+ METADATA_HASH_EXPRESSIONS = {"on_virtual_update", "audits", "signals", "audit_definitions"}
36
+
37
+
38
+ def migrate(state_sync, **kwargs): # type: ignore
39
+ engine_adapter = state_sync.engine_adapter
40
+ schema = state_sync.schema
41
+ snapshots_table = "_snapshots"
42
+ if schema:
43
+ snapshots_table = f"{schema}.{snapshots_table}"
44
+
45
+ warning = (
46
+ "SQLMesh detected that it may not be able to fully migrate the state database. This should not impact "
47
+ "the migration process, but may result in unexpected changes being reported by the next `sqlmesh plan` "
48
+ "command. Please run `sqlmesh diff prod` after the migration has completed, before making any new "
49
+ "changes. If any unexpected changes are reported, consider running a forward-only plan to apply these "
50
+ "changes and avoid unnecessary backfills: sqlmesh plan prod --forward-only. "
51
+ "See https://sqlmesh.readthedocs.io/en/stable/concepts/plans/#forward-only-plans for more details.\n"
52
+ )
53
+
54
+ for (snapshot,) in engine_adapter.fetchall(
55
+ exp.select("snapshot").from_(snapshots_table), quote_identifiers=True
56
+ ):
57
+ parsed_snapshot = json.loads(snapshot)
58
+ node = parsed_snapshot["node"]
59
+
60
+ # Standalone audits don't have a data hash, so they're unaffected
61
+ if node.get("source_type") == "audit":
62
+ continue
63
+
64
+ python_env = node.get("python_env") or {}
65
+
66
+ if SQLMESH_BLUEPRINT_VARS in python_env or (
67
+ SQLMESH_VARS in python_env
68
+ and (
69
+ any(v.get("is_metadata") for v in python_env.values())
70
+ or any(node.get(k) for k in METADATA_HASH_EXPRESSIONS)
71
+ )
72
+ ):
73
+ get_console().log_warning(warning)
74
+ return
sqlmesh/utils/jinja.py CHANGED
@@ -229,7 +229,7 @@ def extract_macro_references_and_variables(
229
229
  )
230
230
 
231
231
  for call_name, node in extract_call_names(jinja_str):
232
- if call_name[0] == c.VAR:
232
+ if call_name[0] in (c.VAR, c.BLUEPRINT_VAR):
233
233
  assert isinstance(node, nodes.Call)
234
234
  args = [jinja_call_arg_name(arg) for arg in node.args]
235
235
  if args and args[0]:
@@ -283,7 +283,7 @@ def build_env(
283
283
  env: t.Dict[str, t.Tuple[t.Any, t.Optional[bool]]],
284
284
  name: str,
285
285
  path: Path,
286
- is_metadata_obj: t.Optional[bool] = None,
286
+ is_metadata_obj: bool = False,
287
287
  ) -> None:
288
288
  """Fills in env dictionary with all globals needed to execute the object.
289
289
 
@@ -299,7 +299,7 @@ def build_env(
299
299
  # We don't rely on `env` to keep track of visited objects, because it's populated in post-order
300
300
  visited: t.Set[str] = set()
301
301
 
302
- def walk(obj: t.Any, name: str, is_metadata: t.Optional[bool] = None) -> None:
302
+ def walk(obj: t.Any, name: str, is_metadata: bool = False) -> None:
303
303
  obj_module = inspect.getmodule(obj)
304
304
  if obj_module and obj_module.__name__ == "builtins":
305
305
  return
@@ -320,7 +320,7 @@ def build_env(
320
320
  # The existing object in the env is "metadata only" but we're walking it again as a
321
321
  # non-"metadata only" dependency, so we update this flag to ensure all transitive
322
322
  # dependencies are also not marked as "metadata only"
323
- is_metadata = None
323
+ is_metadata = False
324
324
 
325
325
  if hasattr(obj, c.SQLMESH_MACRO):
326
326
  # We only need to add the undecorated code of @macro() functions in env, which
@@ -380,7 +380,7 @@ def build_env(
380
380
  )
381
381
 
382
382
  # The "metadata only" annotation of the object is transitive
383
- walk(obj, name, is_metadata_obj or getattr(obj, c.SQLMESH_METADATA, None))
383
+ walk(obj, name, is_metadata_obj or getattr(obj, c.SQLMESH_METADATA, False))
384
384
 
385
385
 
386
386
  @dataclass
@@ -432,7 +432,11 @@ class Executable(PydanticModel):
432
432
  cls, v: t.Any, is_metadata: t.Optional[bool] = None, sort_root_dict: bool = False
433
433
  ) -> Executable:
434
434
  payload = _dict_sort(v) if sort_root_dict else repr(v)
435
- return Executable(payload=payload, kind=ExecutableKind.VALUE, is_metadata=is_metadata)
435
+ return Executable(
436
+ payload=payload,
437
+ kind=ExecutableKind.VALUE,
438
+ is_metadata=is_metadata or None,
439
+ )
436
440
 
437
441
 
438
442
  def serialize_env(env: t.Dict[str, t.Any], path: Path) -> t.Dict[str, Executable]:
@@ -447,6 +451,9 @@ def serialize_env(env: t.Dict[str, t.Any], path: Path) -> t.Dict[str, Executable
447
451
  serialized = {}
448
452
 
449
453
  for k, (v, is_metadata) in env.items():
454
+ # We don't store `False` for `is_metadata` to reduce the pydantic model's payload size
455
+ is_metadata = is_metadata or None
456
+
450
457
  if isinstance(v, LITERALS) or v is None:
451
458
  serialized[k] = Executable.value(v, is_metadata=is_metadata)
452
459
  elif inspect.ismodule(v):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlmesh
3
- Version: 0.209.1.dev12
3
+ Version: 0.209.1.dev13
4
4
  Summary: Next-generation data transformation framework
5
5
  Author-email: "TobikoData Inc." <engineering@tobikodata.com>
6
6
  License: Apache License
@@ -1,5 +1,5 @@
1
1
  sqlmesh/__init__.py,sha256=QjwaamJIdW7voXoS880yAx7tHm-Im4_mqGA-XJgafos,7631
2
- sqlmesh/_version.py,sha256=7OqlZXPJ-sO1ZD5h7xeYT3AcKstkLcz3pDByvyZckQs,723
2
+ sqlmesh/_version.py,sha256=Z-QEmLpn7WCZTovKI1S7q9PMPW5I4VTOb_KNgW5Qf5Y,723
3
3
  sqlmesh/magics.py,sha256=bEzYqXHcM0P3CjyUZ1O9arlmXAdLAhoglKbQ29fD9AE,41842
4
4
  sqlmesh/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  sqlmesh/cicd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -12,14 +12,14 @@ sqlmesh/cli/project_init.py,sha256=5JmDmUK0WJs39hM-p76uzHYdo1Hnx6fKuJg_pR3Hn5A,1
12
12
  sqlmesh/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  sqlmesh/core/_typing.py,sha256=e1f7xO2aZCy38M_3VoDy-XI3shyd99-IFctgIFSgXWM,497
14
14
  sqlmesh/core/console.py,sha256=KbIXoxt5hpDENEolY-DpBlPmcWtJ9vzEgF52mjG2jPE,165710
15
- sqlmesh/core/constants.py,sha256=gUABXWYgH_XOSbaZAJsR5o1dmA61JDCvBDyxqqXX3LM,2630
15
+ sqlmesh/core/constants.py,sha256=Wg6qApCRhENA1me7ELUS5GnArKlTBH-8192gQYKQnE8,2759
16
16
  sqlmesh/core/context.py,sha256=bFAvnjg3YENIgWVDYfflKA5yW6o6gMEZhiSKBPIrBk8,127613
17
17
  sqlmesh/core/context_diff.py,sha256=_Ur55fGLJ58Icsp44I4pVhdi7_JHiHq45mWIco9oFzk,21792
18
18
  sqlmesh/core/dialect.py,sha256=wJ2cDoxz72LIRJ3iBZdCqtnKMbMUdIftl-AvbVldI28,52413
19
19
  sqlmesh/core/environment.py,sha256=odus40R-YzdQdS_IiUq_tFouUXIhnpwmNwo8dE8Aw1k,13041
20
20
  sqlmesh/core/lineage.py,sha256=zjB0Zfamo2Fja2r5SSZPMbrEKTXr1WjozZPVqvxdybI,3143
21
21
  sqlmesh/core/loader.py,sha256=z8KNT5igXc_g-5X1BPPL9XAbpve3ZQp7OkkX63iT0Gc,41179
22
- sqlmesh/core/macros.py,sha256=_tQohZfLoIjO7qpgNu91Qqu3GRJ22k4Ic1AVdowTHuY,62542
22
+ sqlmesh/core/macros.py,sha256=G1TD1Lv-iTNOh1JtHwRFOrnThqDBFuAQgQJtp6UDe0I,63023
23
23
  sqlmesh/core/node.py,sha256=3ChBz1PIMJhp-jzed_GLBrtmEF2v_BVDEM5WNe0ssPk,14658
24
24
  sqlmesh/core/notification_target.py,sha256=PPGoDrgbRKxr27vJEu03XqNTQLYTw0ZF_b0yAapxGeI,16158
25
25
  sqlmesh/core/reference.py,sha256=k7OSkLqTjPR8WJjNeFj0xAJ297nZUMgb_iTVwKRRKjc,4875
@@ -36,7 +36,7 @@ sqlmesh/core/analytics/collector.py,sha256=sggJvER16Jrg5triTC27E2KwaQvfkVDv4D6R1
36
36
  sqlmesh/core/analytics/dispatcher.py,sha256=SQw5tHIETb7ei_oCj7ktHEJyeJNbKyIYiOPxQXyQuIg,6259
37
37
  sqlmesh/core/audit/__init__.py,sha256=3jogKe-G4X0rOxwH5QVuRRioyesQffSWxKU4PVwMPvA,216
38
38
  sqlmesh/core/audit/builtin.py,sha256=adA-cFCphB66jVY4n66cbwbRTFF1FTEIMrLhRDpbBo0,17783
39
- sqlmesh/core/audit/definition.py,sha256=QXXObhR_jHgjWLu0qXxPuIXm0GlFQq33rhp11m_xAUU,18306
39
+ sqlmesh/core/audit/definition.py,sha256=iPpGEOwFIXFHQFQGMyapCm7jnlotwMa-gNaCgTwzlIA,18360
40
40
  sqlmesh/core/config/__init__.py,sha256=5zxTXjUMuxBj4XlX6bmpIWTp4GsZY5rIdKSCTYbxRBM,2070
41
41
  sqlmesh/core/config/base.py,sha256=t8NQmsgQoZSc-k0dlDiCb8t1jj0AMYdGZ-6se9q_Pks,4898
42
42
  sqlmesh/core/config/categorizer.py,sha256=6vzUoNLjR6GOEb_2mYVz2TwmMv2BfldgHX2u-Le5HZs,1975
@@ -86,9 +86,9 @@ sqlmesh/core/metric/definition.py,sha256=Yd5aVgsZCDPJ43aGP7WqtzZOuuSUtB8uJGVA6Jw
86
86
  sqlmesh/core/metric/rewriter.py,sha256=GiSTHfn2kinqCfNPYgZPRk93JFLzVaaejHtHDQ0yXZI,7326
87
87
  sqlmesh/core/model/__init__.py,sha256=C8GRZ53xuXEA9hQv3BQS9pNNyd9rZ06R_B96UYGhDu4,1644
88
88
  sqlmesh/core/model/cache.py,sha256=csun0RJguHzKX6-qITcOs4fVP4f8_Ts8qiUVV4sHY6Q,7869
89
- sqlmesh/core/model/common.py,sha256=Gt1nBdbILfR52Rhq2KxF0EMGkB3J5zfk6UH7ecLpMZI,17969
89
+ sqlmesh/core/model/common.py,sha256=xCMdwkJXnwjE9-56wNb1HiER0KHmWbBzc2Tm--2u168,25610
90
90
  sqlmesh/core/model/decorator.py,sha256=wUUg5t0K559tV9Pqt6FE2EWXt5TPGb4wF2Yg4B4hX4w,9251
91
- sqlmesh/core/model/definition.py,sha256=zDezkLS4FAWkd5OKSTtk8VGmepirq-zGSl4aBsLehLg,113573
91
+ sqlmesh/core/model/definition.py,sha256=uouwIbWA-m2cFXxvDrBVHKxR0ycSAq8Rz3Qv3xY7ewA,113852
92
92
  sqlmesh/core/model/kind.py,sha256=uN73ycANfM94CxcOdkUTclLansNvwo3wySkkl--QzY0,36322
93
93
  sqlmesh/core/model/meta.py,sha256=IE-b_OkCcKzXuQBsopiaf0l5a5Ay903N_64Z1s4cXGk,20064
94
94
  sqlmesh/core/model/schema.py,sha256=_HMYfzK9wWXh7_CQDIIGnuQUD4aiX3o5D2cRp2sERzc,3387
@@ -264,6 +264,7 @@ sqlmesh/migrations/v0084_normalize_quote_when_matched_and_merge_filter.py,sha256
264
264
  sqlmesh/migrations/v0085_deterministic_repr.py,sha256=mkbZV0rzXJ5bmN1Y75kFI9T6gbnx_sy-zn_UzbOL1Sg,4265
265
265
  sqlmesh/migrations/v0086_check_deterministic_bug.py,sha256=WiYcmhQD434GXWiiakQtcy9ESmB35spXMltV43nvU7Q,2953
266
266
  sqlmesh/migrations/v0087_normalize_blueprint_variables.py,sha256=GI6KKA-6PtONUoOnyvLpRTUmPjbpa-CKhDa0iqJW01U,4421
267
+ sqlmesh/migrations/v0088_warn_about_variable_python_env_diffs.py,sha256=zCLT6Yqwny73SlfACWjO1bZfs8lZ7gJBov5Iz4akwiQ,3254
267
268
  sqlmesh/schedulers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
268
269
  sqlmesh/utils/__init__.py,sha256=51VqA2etTZ6kX54250s1hN5ui7D89OeUTUQ9kgMze7s,11554
269
270
  sqlmesh/utils/aws.py,sha256=krxyhrKtsMJDEjT567UbGZogjJ9JRHkvAV-Cg3FVndM,1062
@@ -278,9 +279,9 @@ sqlmesh/utils/date.py,sha256=uS0Kii4hFJeTXqXtM39sDCzvFhTAoPk_NPg51T1YYeo,16218
278
279
  sqlmesh/utils/errors.py,sha256=8FE33Eo3KN3Y4s5EZM5t92jZi9BAVGzkr3QCGlrLeDk,5679
279
280
  sqlmesh/utils/git.py,sha256=v1MD4Zwn52UBn_tTfoKn8SAPJVGC6SEFrJ4WEJFbgF0,1868
280
281
  sqlmesh/utils/hashing.py,sha256=nZRKvLNQ83tLG4IoXshVJZf-MbDrXC1HOeNw8Ji-tMM,578
281
- sqlmesh/utils/jinja.py,sha256=zulSPIICdFOjN__xkAiMpxbCS7S0--xAFK9d8VG0KvU,27844
282
+ sqlmesh/utils/jinja.py,sha256=7IPOu4pjUmxX_ZNL7S0UN23CXJM8EUzPaFt3Wzr1rxI,27863
282
283
  sqlmesh/utils/lineage.py,sha256=zz9BPc6MShRy9LEXmAp02x6oKt4ubVNUPdapFVFKkac,16019
283
- sqlmesh/utils/metaprogramming.py,sha256=yDWS3_tkm6Yd5TNHgii2x_VdZ2pVM0vL6oFuwFMMs0A,23554
284
+ sqlmesh/utils/metaprogramming.py,sha256=6NQKi9PpYhgJEu0Wr_TztRZZ2Kog4Dic9syNqbvztMw,23727
284
285
  sqlmesh/utils/migration.py,sha256=R7Ck6FX-LfcujGy7sgS0kDWlK-xAEBODr3gqIQ8gC08,835
285
286
  sqlmesh/utils/pandas.py,sha256=FytTSLcRNtO0-YMvpoDBkkut3yoQJR9mBSKpif8lxug,2497
286
287
  sqlmesh/utils/process.py,sha256=vpXcpJ1eIbivs7gZdwTKSJI7OS6kKBebnaBzz75ykvs,2428
@@ -288,7 +289,7 @@ sqlmesh/utils/pydantic.py,sha256=o_NsXbIpDqNpUA1Uc5xF0ZzoXQYYB0DfHwdRxBwXPNk,120
288
289
  sqlmesh/utils/rich.py,sha256=cwQ5nJ6sgz64xHtoh6_ec7ReV5YpsOGhMtUJnwoRfEI,3549
289
290
  sqlmesh/utils/windows.py,sha256=r7Yin4WEwbvH7sKA3hOJtRsclmscNuBeCklCfVPvtQs,611
290
291
  sqlmesh/utils/yaml.py,sha256=hvo5lmybYLcIx4p6CQwufS8X4MbwS60lEBnCmYJCXmU,2336
291
- sqlmesh-0.209.1.dev12.dist-info/licenses/LICENSE,sha256=OlMefUjgWJdULtf84BLW0AZZcY8DwdgQqb_1j2862j8,11346
292
+ sqlmesh-0.209.1.dev13.dist-info/licenses/LICENSE,sha256=OlMefUjgWJdULtf84BLW0AZZcY8DwdgQqb_1j2862j8,11346
292
293
  sqlmesh_dbt/__init__.py,sha256=awYS5y5mz-1NUmx6i5h5NSTJ7tidRl9NC0FAnFWSF6U,350
293
294
  sqlmesh_dbt/cli.py,sha256=Ekn77WLMmyyr31aoDoWrZp5DS0rfiddy7pNohBK8G0s,2115
294
295
  sqlmesh_dbt/console.py,sha256=1B0B5_R1DlrEkyWVDl8G78-B6_K-ZtLPsQEhqPCfLEU,188
@@ -410,8 +411,8 @@ web/server/api/endpoints/models.py,sha256=kwj0s7uve3iZSMfmjkoPVMFMeY1sD0peTeyrWf
410
411
  web/server/api/endpoints/modules.py,sha256=8hqqgonGay_mJmpCw0IdbjsPhWlQH2VLdKAqha-myac,468
411
412
  web/server/api/endpoints/plan.py,sha256=bbbY50W_2MsZSTxOHWMKz0tbIm75nsRSlPy8GI2fg9Q,9306
412
413
  web/server/api/endpoints/table_diff.py,sha256=SCy3-pr5x6bEEiOaZGCDbiHQhpgxwmnz3Acqa2Ie6jI,2090
413
- sqlmesh-0.209.1.dev12.dist-info/METADATA,sha256=M3xduNBEn62vHm2RLj8dwbolcTWTz77Rk0Mlu3z_tOY,26686
414
- sqlmesh-0.209.1.dev12.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
415
- sqlmesh-0.209.1.dev12.dist-info/entry_points.txt,sha256=sHAf6tQczIM8xZoduN4qaUjV7QEPVUUW_LCT8EDUMv4,155
416
- sqlmesh-0.209.1.dev12.dist-info/top_level.txt,sha256=RQ-33FPe2IgL0rgossAfJkCRtqslz9b7wFARqiWLC5Q,24
417
- sqlmesh-0.209.1.dev12.dist-info/RECORD,,
414
+ sqlmesh-0.209.1.dev13.dist-info/METADATA,sha256=xyA8vDp0_wasj_KFmp6AZoegsW7YBPLiaOnzE_ndzf4,26686
415
+ sqlmesh-0.209.1.dev13.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
416
+ sqlmesh-0.209.1.dev13.dist-info/entry_points.txt,sha256=sHAf6tQczIM8xZoduN4qaUjV7QEPVUUW_LCT8EDUMv4,155
417
+ sqlmesh-0.209.1.dev13.dist-info/top_level.txt,sha256=RQ-33FPe2IgL0rgossAfJkCRtqslz9b7wFARqiWLC5Q,24
418
+ sqlmesh-0.209.1.dev13.dist-info/RECORD,,