Skip to content

Commit 2bca344

Browse files
committed
feat: location in nomissingexternal models linting error
1 parent 8f2947b commit 2bca344

File tree

11 files changed

+584
-404
lines changed

11 files changed

+584
-404
lines changed

sqlmesh/core/linter/helpers.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -84,19 +84,8 @@ def to_range(self, read_file: t.Optional[t.List[str]]) -> Range:
8484
)
8585

8686

87-
def read_range_from_file(file: Path, text_range: Range) -> str:
88-
"""
89-
Read the file and return the content within the specified range.
90-
91-
Args:
92-
file: Path to the file to read
93-
text_range: The range of text to extract
94-
95-
Returns:
96-
The content within the specified range
97-
"""
98-
with file.open("r", encoding="utf-8") as f:
99-
lines = f.readlines()
87+
def read_range_from_string(content: str, text_range: Range) -> str:
88+
lines = content.splitlines(keepends=False)
10089

10190
# Ensure the range is within bounds
10291
start_line = max(0, text_range.start.line)
@@ -116,6 +105,23 @@ def read_range_from_file(file: Path, text_range: Range) -> str:
116105
return "".join(result)
117106

118107

108+
def read_range_from_file(file: Path, text_range: Range) -> str:
109+
"""
110+
Read the file and return the content within the specified range.
111+
112+
Args:
113+
file: Path to the file to read
114+
text_range: The range of text to extract
115+
116+
Returns:
117+
The content within the specified range
118+
"""
119+
with file.open("r", encoding="utf-8") as f:
120+
lines = f.readlines()
121+
122+
return read_range_from_string("".join(lines), text_range)
123+
124+
119125
def get_range_of_model_block(
120126
sql: str,
121127
dialect: str,

sqlmesh/core/linter/rules/builtin.py

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,16 @@
77
from sqlglot.expressions import Star
88
from sqlglot.helper import subclasses
99

10-
from sqlmesh.core.linter.helpers import TokenPositionDetails, get_range_of_model_block
10+
from sqlmesh.core.dialect import normalize_model_name
11+
from sqlmesh.core.linter.helpers import (
12+
TokenPositionDetails,
13+
get_range_of_model_block,
14+
read_range_from_string,
15+
)
1116
from sqlmesh.core.linter.rule import Rule, RuleViolation, Range, Fix, TextEdit
1217
from sqlmesh.core.linter.definition import RuleSet
1318
from sqlmesh.core.model import Model, SqlModel, ExternalModel
19+
from sqlmesh.utils.lineage import extract_references_from_query, LSPExternalModelReference
1420

1521

1622
class NoSelectStar(Rule):
@@ -113,7 +119,9 @@ def check_model(self, model: Model) -> t.Optional[RuleViolation]:
113119
class NoMissingExternalModels(Rule):
114120
"""All external models must be registered in the external_models.yaml file"""
115121

116-
def check_model(self, model: Model) -> t.Optional[RuleViolation]:
122+
def check_model(
123+
self, model: Model
124+
) -> t.Optional[t.Union[RuleViolation, t.List[RuleViolation]]]:
117125
# Ignore external models themselves, because either they are registered,
118126
# and if they are not, they will be caught as referenced in another model.
119127
if isinstance(model, ExternalModel):
@@ -129,10 +137,74 @@ def check_model(self, model: Model) -> t.Optional[RuleViolation]:
129137
if not not_registered_external_models:
130138
return None
131139

140+
# If the model is anything other than a sql model that and has a path
141+
# that ends with .sql, we cannot extract the references from the query.
142+
path = getattr(model, "_path", None)
143+
if not isinstance(model, SqlModel) or not path or not str(path).endswith(".sql"):
144+
return self._standard_error_message(
145+
model_name=model.fqn,
146+
external_models=not_registered_external_models,
147+
)
148+
149+
with open(path, "r", encoding="utf-8") as file:
150+
read_file = file.read()
151+
split_read_file = read_file.splitlines()
152+
153+
# If there are any unregistered external models, return a violation find
154+
# the ranges for them.
155+
references = extract_references_from_query(
156+
query=model.query,
157+
context=self.context,
158+
document_path=path,
159+
read_file=split_read_file,
160+
depends_on=model.depends_on,
161+
dialect=model.dialect,
162+
)
163+
external_references = {
164+
normalize_model_name(
165+
table=read_range_from_string(read_file, ref.range),
166+
default_catalog=model.default_catalog,
167+
dialect=model.dialect,
168+
): ref
169+
for ref in references
170+
if isinstance(ref, LSPExternalModelReference) and ref.path is None
171+
}
172+
173+
# Ensure that depends_on are a subset of the external references. If not, return generic violation.
174+
if not_registered_external_models != set(external_references.keys()):
175+
return self._standard_error_message(
176+
model_name=model.fqn,
177+
external_models=not_registered_external_models,
178+
)
179+
180+
# Return a violation for each unregistered external model with its range.
181+
violations = []
182+
for ref_name, ref in external_references.items():
183+
if ref_name in not_registered_external_models:
184+
violations.append(
185+
RuleViolation(
186+
rule=self,
187+
violation_msg=f"Model '{model.fqn}' depends on unregistered external model '{ref_name}'. "
188+
"Please register it in the external models file. This can be done by running 'sqlmesh create_external_models'.",
189+
violation_range=ref.range,
190+
)
191+
)
192+
193+
if len(violations) < len(not_registered_external_models):
194+
return self._standard_error_message(
195+
model_name=model.fqn,
196+
external_models=not_registered_external_models,
197+
)
198+
199+
return violations
200+
201+
def _standard_error_message(
202+
self, model_name: str, external_models: t.Set[str]
203+
) -> RuleViolation:
132204
return RuleViolation(
133205
rule=self,
134-
violation_msg=f"Model '{model.name}' depends on unregistered external models: "
135-
f"{', '.join(m for m in not_registered_external_models)}. "
206+
violation_msg=f"Model '{model_name}' depends on unregistered external models: "
207+
f"{', '.join(m for m in external_models)}. "
136208
"Please register them in the external models file. This can be done by running 'sqlmesh create_external_models'.",
137209
)
138210

sqlmesh/lsp/completions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
from sqlmesh import macro
99
import typing as t
1010
from sqlmesh.lsp.context import AuditTarget, LSPContext, ModelTarget
11-
from sqlmesh.lsp.description import generate_markdown_description
1211
from sqlmesh.lsp.uri import URI
12+
from sqlmesh.utils.lineage import generate_markdown_description
1313

1414

1515
def get_sql_completions(

sqlmesh/lsp/description.py

Lines changed: 0 additions & 29 deletions
This file was deleted.

sqlmesh/lsp/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,13 @@
6262
from sqlmesh.lsp.reference import (
6363
LSPCteReference,
6464
LSPModelReference,
65-
LSPExternalModelReference,
6665
get_references,
6766
get_all_references,
6867
)
6968
from sqlmesh.lsp.rename import prepare_rename, rename_symbol, get_document_highlights
7069
from sqlmesh.lsp.uri import URI
7170
from sqlmesh.utils.errors import ConfigError
71+
from sqlmesh.utils.lineage import LSPExternalModelReference
7272
from web.server.api.endpoints.lineage import column_lineage, model_lineage
7373
from web.server.api.endpoints.models import get_models
7474
from typing import Union

0 commit comments

Comments
 (0)