Skip to content

Commit 555f1fe

Browse files
committed
feat: location in nomissingexternal models linting error
1 parent b0ad05b commit 555f1fe

File tree

10 files changed

+501
-400
lines changed

10 files changed

+501
-400
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: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,15 @@
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.linter.helpers import (
11+
TokenPositionDetails,
12+
get_range_of_model_block,
13+
read_range_from_string,
14+
)
1115
from sqlmesh.core.linter.rule import Rule, RuleViolation, Range, Fix, TextEdit
1216
from sqlmesh.core.linter.definition import RuleSet
1317
from sqlmesh.core.model import Model, SqlModel, ExternalModel
18+
from sqlmesh.utils.lineage import extract_references_from_query, LSPExternalModelReference
1419

1520

1621
class NoSelectStar(Rule):
@@ -113,7 +118,9 @@ def check_model(self, model: Model) -> t.Optional[RuleViolation]:
113118
class NoMissingExternalModels(Rule):
114119
"""All external models must be registered in the external_models.yaml file"""
115120

116-
def check_model(self, model: Model) -> t.Optional[RuleViolation]:
121+
def check_model(
122+
self, model: Model
123+
) -> t.Optional[t.Union[RuleViolation, t.List[RuleViolation]]]:
117124
# Ignore external models themselves, because either they are registered,
118125
# and if they are not, they will be caught as referenced in another model.
119126
if isinstance(model, ExternalModel):
@@ -129,10 +136,64 @@ def check_model(self, model: Model) -> t.Optional[RuleViolation]:
129136
if not not_registered_external_models:
130137
return None
131138

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

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)