Skip to content

Commit c5334d6

Browse files
committed
feat: linting rule no unregistered external models
1 parent afb7907 commit c5334d6

File tree

3 files changed

+76
-2
lines changed

3 files changed

+76
-2
lines changed

examples/sushi/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
"noselectstar",
4949
"nomissingaudits",
5050
"nomissingowner",
51+
"nounregisteredexternalmodels",
5152
],
5253
),
5354
)

sqlmesh/core/linter/rules/builtin.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
from sqlmesh.core.linter.helpers import TokenPositionDetails, get_range_of_model_block
1111
from sqlmesh.core.linter.rule import Rule, RuleViolation, Range, Fix, TextEdit
1212
from sqlmesh.core.linter.definition import RuleSet
13-
from sqlmesh.core.model import Model, SqlModel
13+
from sqlmesh.core.model import Model, SqlModel, ExternalModel
1414

1515

1616
class NoSelectStar(Rule):
17-
"""Query should not contain SELECT * on its outer most projections, even if it can be expanded."""
17+
"""Query should not contain SELECT * on its outermost projections, even if it can be expanded."""
1818

1919
def check_model(self, model: Model) -> t.Optional[RuleViolation]:
2020
# Only applies to SQL models, as other model types do not have a query.
@@ -110,4 +110,33 @@ def check_model(self, model: Model) -> t.Optional[RuleViolation]:
110110
return self.violation()
111111

112112

113+
class NoUnregisteredExternalModels(Rule):
114+
"""All external models must be registered in the external_models.yaml file"""
115+
116+
def check_model(self, model: Model) -> t.Optional[RuleViolation]:
117+
depends_on = model.depends_on
118+
119+
# Ignore external models themselves, because either they are registered
120+
# if they are not, they will be caught as referenced in another model.
121+
if isinstance(model, ExternalModel):
122+
return None
123+
124+
# Handle other models that are referring to them
125+
not_registered_external_models: t.Set[str] = set()
126+
for depends_on_model in depends_on:
127+
existing_model = self.context.get_model(depends_on_model)
128+
if existing_model is None:
129+
not_registered_external_models.add(depends_on_model)
130+
131+
if not not_registered_external_models:
132+
return None
133+
134+
return RuleViolation(
135+
rule=self,
136+
violation_msg=f"Model '{model.name}' depends on unregistered external models: "
137+
f"{', '.join(m for m in not_registered_external_models)}. "
138+
"Please register them in the external_models.yaml file.",
139+
)
140+
141+
113142
BUILTIN_RULES = RuleSet(subclasses(__name__, Rule, (Rule,)))

tests/core/linter/test_builtin.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import os
2+
3+
from sqlmesh import Context
4+
5+
6+
def test_no_unregistered_external_models(tmp_path, copy_to_temp_path) -> None:
7+
"""Tests that it returns a correct"""
8+
sushi_paths = copy_to_temp_path("examples/sushi")
9+
sushi_path = sushi_paths[0]
10+
11+
# Remove the external_models.yaml file
12+
os.remove(sushi_path / "external_models.yaml")
13+
14+
# Override the config.py to turn on lint
15+
with open(sushi_path / "config.py", "r") as f:
16+
read_file = f.read()
17+
18+
before = """ linter=LinterConfig(
19+
enabled=False,
20+
rules=[
21+
"ambiguousorinvalidcolumn",
22+
"invalidselectstarexpansion",
23+
"noselectstar",
24+
"nomissingaudits",
25+
"nomissingowner",
26+
"nounregisteredexternalmodels",
27+
],
28+
),"""
29+
after = """linter=LinterConfig(enabled=True, rules=["nounregisteredexternalmodels"]),"""
30+
read_file = read_file.replace(before, after)
31+
assert after in read_file
32+
with open(sushi_path / "config.py", "w") as f:
33+
f.writelines(read_file)
34+
35+
# Load the context with the temporary sushi path
36+
context = Context(paths=[sushi_path])
37+
38+
# Lint the models
39+
lints = context.lint_models(raise_on_error=False)
40+
assert len(lints) == 1
41+
assert (
42+
"Model 'sushi.customers' depends on unregistered external models: "
43+
in lints[0].violation_msg
44+
)

0 commit comments

Comments
 (0)