Skip to content

Commit 347e312

Browse files
committed
cron validator builtin
1 parent c72d253 commit 347e312

6 files changed

Lines changed: 93 additions & 0 deletions

File tree

examples/sushi/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"nomissingaudits",
5050
"nomissingowner",
5151
"nomissingexternalmodels",
52+
"cronvalidator",
5253
],
5354
),
5455
)

sqlmesh/core/linter/rules/builtin.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,4 +274,34 @@ def create_fix(self, model_name: str) -> t.Optional[Fix]:
274274
)
275275

276276

277+
class CronValidator(Rule):
278+
"""Upstream model has a cron expression with longer intervals than this model's."""
279+
280+
def check_model(self, model: Model) -> t.Optional[RuleViolation]:
281+
placeholder_start_date = "2020-01-01 10:00:00"
282+
283+
this_model_cron_next = model.cron_next(placeholder_start_date)
284+
285+
for upstream_model_name in model.depends_on:
286+
upstream_model = self.context.get_model(upstream_model_name)
287+
288+
# Skip if upstream model doesn't exist
289+
if upstream_model is None:
290+
continue
291+
292+
# Skip model kinds since they don't run on cron schedules
293+
skip_kinds = ["EXTERNAL", "EMBEDDED", "SEED"]
294+
if upstream_model.kind.name in skip_kinds:
295+
continue
296+
297+
upstream_model_cron_next = upstream_model.cron_next(placeholder_start_date)
298+
299+
if upstream_model_cron_next > this_model_cron_next:
300+
return self.violation(
301+
f"Upstream model {upstream_model_name} has longer cron interval ({upstream_model.cron}) "
302+
f"than this model ({model.cron})"
303+
)
304+
return None
305+
306+
277307
BUILTIN_RULES = RuleSet(subclasses(__name__, Rule, (Rule,)))

tests/core/linter/test_builtin.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from sqlmesh import Context
44
from sqlmesh.core.linter.rule import Position, Range
5+
from sqlmesh.core.model import load_sql_based_model
6+
from sqlmesh.core import dialect as d
57

68

79
def test_no_missing_external_models(tmp_path, copy_to_temp_path) -> None:
@@ -31,6 +33,7 @@ def test_no_missing_external_models(tmp_path, copy_to_temp_path) -> None:
3133
"nomissingaudits",
3234
"nomissingowner",
3335
"nomissingexternalmodels",
36+
"cronvalidator",
3437
],
3538
),"""
3639
after = """linter=LinterConfig(enabled=True, rules=["nomissingexternalmodels"]),"""
@@ -84,6 +87,7 @@ def test_no_missing_external_models_with_existing_file_ending_in_newline(
8487
"nomissingaudits",
8588
"nomissingowner",
8689
"nomissingexternalmodels",
90+
"cronvalidator",
8791
],
8892
),"""
8993
after = """linter=LinterConfig(enabled=True, rules=["nomissingexternalmodels"]),"""
@@ -141,6 +145,7 @@ def test_no_missing_external_models_with_existing_file_not_ending_in_newline(
141145
"nomissingaudits",
142146
"nomissingowner",
143147
"nomissingexternalmodels",
148+
"cronvalidator",
144149
],
145150
),"""
146151
after = """linter=LinterConfig(enabled=True, rules=["nomissingexternalmodels"]),"""
@@ -172,3 +177,57 @@ def test_no_missing_external_models_with_existing_file_not_ending_in_newline(
172177
)
173178
fix_path = sushi_path / "external_models.yaml"
174179
assert edit.path == fix_path
180+
181+
182+
def test_cron_validator(tmp_path, copy_to_temp_path) -> None:
183+
sushi_paths = copy_to_temp_path("examples/sushi")
184+
sushi_path = sushi_paths[0]
185+
186+
# Override the config.py to turn on lint
187+
with open(sushi_path / "config.py", "r") as f:
188+
read_file = f.read()
189+
190+
before = """ linter=LinterConfig(
191+
enabled=False,
192+
rules=[
193+
"ambiguousorinvalidcolumn",
194+
"invalidselectstarexpansion",
195+
"noselectstar",
196+
"nomissingaudits",
197+
"nomissingowner",
198+
"nomissingexternalmodels",
199+
"cronvalidator",
200+
],
201+
),"""
202+
after = """linter=LinterConfig(enabled=True, rules=["cronvalidator"]),"""
203+
read_file = read_file.replace(before, after)
204+
assert after in read_file
205+
with open(sushi_path / "config.py", "w") as f:
206+
f.writelines(read_file)
207+
208+
# Load the context with the temporary sushi path
209+
context = Context(paths=[sushi_path])
210+
211+
context.load()
212+
213+
# Create model with shorter cron interval that depends on model with longer interval
214+
upstream_model = load_sql_based_model(
215+
d.parse("MODEL (name memory.sushi.step_1, cron '@weekly'); SELECT * FROM (SELECT 1)")
216+
)
217+
218+
downstream_model = load_sql_based_model(
219+
d.parse(
220+
"MODEL (name memory.sushi.step_2, cron '@daily', depends_on ['memory.sushi.step_1']); SELECT * FROM (SELECT 1)"
221+
)
222+
)
223+
224+
context.upsert_model(upstream_model)
225+
context.upsert_model(downstream_model)
226+
227+
lints = context.lint_models(raise_on_error=False)
228+
assert len(lints) == 1
229+
lint = lints[0]
230+
assert (
231+
lint.violation_msg
232+
== 'Upstream model "memory"."sushi"."step_1" has longer cron interval (@weekly) than this model (@daily)'
233+
)

tests/lsp/test_code_actions.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ def test_code_actions_create_file(copy_to_temp_path: t.Callable) -> None:
131131
"nomissingaudits",
132132
"nomissingowner",
133133
"nomissingexternalmodels",
134+
"cronvalidator",
134135
],
135136
),"""
136137
after = """linter=LinterConfig(enabled=True, rules=["nomissingexternalmodels"]),"""

tests/lsp/test_reference_external_model.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ def test_unregistered_external_model_with_schema(
9494
"nomissingaudits",
9595
"nomissingowner",
9696
"nomissingexternalmodels",
97+
"cronvalidator",
9798
],
9899
),"""
99100
after = """linter=LinterConfig(enabled=True, rules=["nomissingexternalmodels"]),"""

vscode/extension/tests/quickfix.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ test.fixme(
3535
"nomissingaudits",
3636
"nomissingowner",
3737
"nomissingexternalmodels",
38+
"cronvalidator",
3839
],`,
3940
targetRules,
4041
)

0 commit comments

Comments
 (0)