Skip to content

Commit 44b8fa9

Browse files
authored
Feat: Introduce metadata only flag for macro functions (#2833)
1 parent ca05f9b commit 44b8fa9

File tree

8 files changed

+121
-7
lines changed

8 files changed

+121
-7
lines changed

sqlmesh/core/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060

6161
SQLMESH_MACRO = "__sqlmesh__macro__"
6262
SQLMESH_BUILTIN = "__sqlmesh__builtin__"
63+
SQLMESH_METADATA = "__sqlmesh__metadata__"
6364

6465

6566
BUILTIN = "builtin"

sqlmesh/core/macros.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -558,9 +558,15 @@ def add_one(evaluator: MacroEvaluator, column: exp.Literal) -> exp.Add:
558558

559559
registry_name = "macros"
560560

561+
def __init__(self, *args: t.Any, is_metadata: bool = False, **kwargs: t.Any) -> None:
562+
super().__init__(*args, **kwargs)
563+
self.is_metadata = is_metadata
564+
561565
def __call__(
562566
self, func: t.Callable[..., DECORATOR_RETURN_TYPE]
563567
) -> t.Callable[..., DECORATOR_RETURN_TYPE]:
568+
if self.is_metadata:
569+
setattr(func, c.SQLMESH_METADATA, self.is_metadata)
564570
wrapper = super().__call__(func)
565571

566572
# This is used to identify macros at runtime to unwrap during serialization.

sqlmesh/core/model/definition.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -962,18 +962,40 @@ def _data_hash_values(self) -> t.List[str]:
962962

963963
for statement in (*self.pre_statements, *self.post_statements):
964964
statement_exprs: t.List[exp.Expression] = []
965-
if isinstance(statement, d.MacroDef):
966-
statement_exprs = [statement]
967-
else:
965+
if not isinstance(statement, d.MacroDef):
968966
rendered = self._statement_renderer(statement).render()
969-
if rendered is not None:
967+
if self._is_metadata_statement(statement):
968+
continue
969+
if rendered:
970970
statement_exprs = rendered
971971
else:
972972
statement_exprs = [statement]
973973
data_hash_values.extend(gen(e) for e in statement_exprs)
974974

975975
return data_hash_values
976976

977+
@property
978+
def _additional_metadata(self) -> t.List[str]:
979+
additional_metadata = super()._additional_metadata
980+
981+
for statement in (*self.pre_statements, *self.post_statements):
982+
if self._is_metadata_statement(statement):
983+
additional_metadata.append(gen(statement))
984+
985+
return additional_metadata
986+
987+
def _is_metadata_statement(self, statement: exp.Expression) -> bool:
988+
if isinstance(statement, d.MacroDef):
989+
return True
990+
if isinstance(statement, d.MacroFunc):
991+
target_macro = macro.get_registry().get(statement.name)
992+
if target_macro:
993+
return target_macro.is_metadata
994+
target_macro = self.python_env.get(statement.name)
995+
if isinstance(target_macro, Executable):
996+
return bool(target_macro.is_metadata)
997+
return False
998+
977999

9781000
class SqlModel(_SqlBasedModel):
9791001
"""The model definition which relies on a SQL query to fetch the data.

sqlmesh/utils/metaprogramming.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,7 @@ class Executable(PydanticModel):
314314
name: t.Optional[str] = None
315315
path: t.Optional[str] = None
316316
alias: t.Optional[str] = None
317+
is_metadata: t.Optional[bool] = None
317318

318319
@property
319320
def is_definition(self) -> bool:
@@ -360,6 +361,7 @@ def serialize_env(env: t.Dict[str, t.Any], path: Path) -> t.Dict[str, Executable
360361
# Do `as_posix` to serialize windows path back to POSIX
361362
path=t.cast(Path, file_path).relative_to(path.absolute()).as_posix(),
362363
alias=k if name != k else None,
364+
is_metadata=getattr(v, c.SQLMESH_METADATA, None),
363365
)
364366
else:
365367
serialized[k] = Executable(

tests/core/test_macros.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytest
44
from sqlglot import MappingSchema, ParseError, exp, parse_one
55

6+
from sqlmesh.core import constants as c
67
from sqlmesh.core.dialect import StagedFilePath
78
from sqlmesh.core.macros import SQL, MacroEvalError, MacroEvaluator, macro
89
from sqlmesh.utils.errors import SQLMeshError
@@ -599,3 +600,19 @@ def test_macro_parameter_resolution(macro_evaluator):
599600
with pytest.raises(MacroEvalError) as e:
600601
macro_evaluator.evaluate(parse_one("@test_arg_resolution(1, 2, 3)"))
601602
assert str(e.value.__cause__) == "too many positional arguments"
603+
604+
605+
def test_macro_metadata_flag():
606+
@macro()
607+
def noop(evaluator) -> None:
608+
return None
609+
610+
@macro(is_metadata=True)
611+
def noop_metadata_only(evaluator) -> None:
612+
return None
613+
614+
assert not hasattr(noop, c.SQLMESH_METADATA)
615+
assert not macro.get_registry()["noop"].is_metadata
616+
617+
assert getattr(noop_metadata_only, c.SQLMESH_METADATA) is True
618+
assert macro.get_registry()["noop_metadata_only"].is_metadata is True

tests/core/test_model.py

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1616,7 +1616,7 @@ def my_model(context, **kwargs):
16161616
WHERE
16171617
FALSE
16181618
LIMIT 0
1619-
""",
1619+
""",
16201620
)
16211621

16221622

@@ -5096,3 +5096,50 @@ def test_model_kind_to_expression():
50965096
materialized TRUE
50975097
)"""
50985098
)
5099+
5100+
5101+
@pytest.mark.parametrize(
5102+
"is_metadata",
5103+
[True, False],
5104+
)
5105+
def test_macro_func_hash(is_metadata):
5106+
macro.set_registry({})
5107+
5108+
@macro(is_metadata=is_metadata)
5109+
def noop(evaluator) -> None:
5110+
return None
5111+
5112+
# Prevent macro from entering python_env so data hash is consistent
5113+
setattr(macro.get_registry()["noop"], c.SQLMESH_BUILTIN, True)
5114+
5115+
expressions = d.parse(
5116+
"""
5117+
MODEL (
5118+
name db.model,
5119+
);
5120+
5121+
SELECT 1;
5122+
"""
5123+
)
5124+
model = load_sql_based_model(expressions, path=Path("./examples/sushi/models/test_model.sql"))
5125+
5126+
expressions = d.parse(
5127+
"""
5128+
MODEL (
5129+
name db.model,
5130+
);
5131+
5132+
SELECT 1;
5133+
5134+
@noop();
5135+
"""
5136+
)
5137+
new_model = load_sql_based_model(
5138+
expressions, path=Path("./examples/sushi/models/test_model.sql")
5139+
)
5140+
if is_metadata:
5141+
assert model.data_hash == new_model.data_hash
5142+
assert model.metadata_hash(audits={}) != new_model.metadata_hash(audits={})
5143+
else:
5144+
assert model.data_hash != new_model.data_hash
5145+
assert model.metadata_hash(audits={}) == new_model.metadata_hash(audits={})

tests/schedulers/airflow/test_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ def test_apply_plan(mocker: MockerFixture, snapshot: Snapshot):
174174
"models_to_backfill": ['"test_model"'],
175175
"end_bounded": False,
176176
"ensure_finalized_snapshots": False,
177-
"directly_modified_snapshots": [{"identifier": "349602315", "name": '"test_model"'}],
177+
"directly_modified_snapshots": [{"identifier": "844700562", "name": '"test_model"'}],
178178
"indirectly_modified_snapshots": {},
179179
"removed_snapshots": [],
180180
"restatements": {'"test_model"': [to_timestamp("2024-01-01"), to_timestamp("2024-01-02")]},

tests/utils/test_metaprogramming.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from sqlglot.expressions import to_table
1010

1111
import tests.utils.test_date as test_date
12+
from sqlmesh.core import constants as c
1213
from sqlmesh.utils.errors import SQLMeshError
1314
from sqlmesh.utils.metaprogramming import (
1415
Executable,
@@ -41,7 +42,7 @@ def test_print_exception(mocker: MockerFixture):
4142

4243
expected_message = f"""Traceback (most recent call last):
4344
44-
File "{__file__}", line 38, in test_print_exception
45+
File "{__file__}", line 39, in test_print_exception
4546
eval("test_fun()", env)
4647
4748
File "<string>", line 1, in <module>
@@ -95,11 +96,19 @@ def other_func(a: int) -> int:
9596
return X + a
9697

9798

99+
def noop_metadata() -> None:
100+
return None
101+
102+
103+
setattr(noop_metadata, c.SQLMESH_METADATA, True)
104+
105+
98106
def main_func(y: int) -> int:
99107
"""DOC STRING"""
100108
sqlglot.parse_one("1")
101109
MyClass()
102110
DataClass(x=y)
111+
noop_metadata()
103112

104113
def closure(z: int) -> int:
105114
return z + Z
@@ -113,6 +122,7 @@ def test_func_globals() -> None:
113122
"Z": 3,
114123
"DataClass": DataClass,
115124
"MyClass": MyClass,
125+
"noop_metadata": noop_metadata,
116126
"other_func": other_func,
117127
"sqlglot": sqlglot,
118128
}
@@ -144,6 +154,7 @@ def test_normalize_source() -> None:
144154
sqlglot.parse_one('1')
145155
MyClass()
146156
DataClass(x=y)
157+
noop_metadata()
147158
148159
def closure(z: int):
149160
return z + Z
@@ -183,6 +194,7 @@ def test_serialize_env() -> None:
183194
sqlglot.parse_one('1')
184195
MyClass()
185196
DataClass(x=y)
197+
noop_metadata()
186198
187199
def closure(z: int):
188200
return z + Z
@@ -233,6 +245,13 @@ def baz(self):
233245
path="test_metaprogramming.py",
234246
payload="my_lambda = lambda : print('z')",
235247
),
248+
"noop_metadata": Executable(
249+
name="noop_metadata",
250+
path="test_metaprogramming.py",
251+
payload="""def noop_metadata():
252+
return None""",
253+
is_metadata=True,
254+
),
236255
"other_func": Executable(
237256
name="other_func",
238257
path="test_metaprogramming.py",

0 commit comments

Comments
 (0)