Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 19 additions & 13 deletions sqlmesh/core/linter/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,19 +84,8 @@ def to_range(self, read_file: t.Optional[t.List[str]]) -> Range:
)


def read_range_from_file(file: Path, text_range: Range) -> str:
"""
Read the file and return the content within the specified range.
Args:
file: Path to the file to read
text_range: The range of text to extract
Returns:
The content within the specified range
"""
with file.open("r", encoding="utf-8") as f:
lines = f.readlines()
def read_range_from_string(content: str, text_range: Range) -> str:
lines = content.splitlines(keepends=False)

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


def read_range_from_file(file: Path, text_range: Range) -> str:
"""
Read the file and return the content within the specified range.
Args:
file: Path to the file to read
text_range: The range of text to extract
Returns:
The content within the specified range
"""
with file.open("r", encoding="utf-8") as f:
lines = f.readlines()

return read_range_from_string("".join(lines), text_range)


def get_range_of_model_block(
sql: str,
dialect: str,
Expand Down
80 changes: 76 additions & 4 deletions sqlmesh/core/linter/rules/builtin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@
from sqlglot.expressions import Star
from sqlglot.helper import subclasses

from sqlmesh.core.linter.helpers import TokenPositionDetails, get_range_of_model_block
from sqlmesh.core.dialect import normalize_model_name
from sqlmesh.core.linter.helpers import (
TokenPositionDetails,
get_range_of_model_block,
read_range_from_string,
)
from sqlmesh.core.linter.rule import Rule, RuleViolation, Range, Fix, TextEdit
from sqlmesh.core.linter.definition import RuleSet
from sqlmesh.core.model import Model, SqlModel, ExternalModel
from sqlmesh.utils.lineage import extract_references_from_query, ExternalModelReference


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

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

# If the model is anything other than a sql model that and has a path
# that ends with .sql, we cannot extract the references from the query.
path = model._path
if not isinstance(model, SqlModel) or not path or not str(path).endswith(".sql"):
return self._standard_error_message(
model_name=model.fqn,
external_models=not_registered_external_models,
)

with open(path, "r", encoding="utf-8") as file:
read_file = file.read()
split_read_file = read_file.splitlines()

# If there are any unregistered external models, return a violation find
# the ranges for them.
references = extract_references_from_query(
query=model.query,
context=self.context,
document_path=path,
read_file=split_read_file,
depends_on=model.depends_on,
dialect=model.dialect,
)
external_references = {
normalize_model_name(
table=read_range_from_string(read_file, ref.range),
default_catalog=model.default_catalog,
dialect=model.dialect,
): ref
for ref in references
if isinstance(ref, ExternalModelReference) and ref.path is None
}

# Ensure that depends_on and external references match.
if not_registered_external_models != set(external_references.keys()):
return self._standard_error_message(
model_name=model.fqn,
external_models=not_registered_external_models,
)

# Return a violation for each unregistered external model with its range.
violations = []
for ref_name, ref in external_references.items():
if ref_name in not_registered_external_models:
violations.append(
RuleViolation(
rule=self,
violation_msg=f"Model '{model.fqn}' depends on unregistered external model '{ref_name}'. "
"Please register it in the external models file. This can be done by running 'sqlmesh create_external_models'.",
violation_range=ref.range,
)
)

if len(violations) < len(not_registered_external_models):
return self._standard_error_message(
model_name=model.fqn,
external_models=not_registered_external_models,
)

return violations

def _standard_error_message(
self, model_name: str, external_models: t.Set[str]
) -> RuleViolation:
return RuleViolation(
rule=self,
violation_msg=f"Model '{model.name}' depends on unregistered external models: "
f"{', '.join(m for m in not_registered_external_models)}. "
violation_msg=f"Model '{model_name}' depends on unregistered external models: "
f"{', '.join(m for m in external_models)}. "
"Please register them in the external models file. This can be done by running 'sqlmesh create_external_models'.",
)

Expand Down
2 changes: 1 addition & 1 deletion sqlmesh/lsp/completions.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
from sqlmesh import macro
import typing as t
from sqlmesh.lsp.context import AuditTarget, LSPContext, ModelTarget
from sqlmesh.lsp.description import generate_markdown_description
from sqlmesh.lsp.uri import URI
from sqlmesh.utils.lineage import generate_markdown_description


def get_sql_completions(
Expand Down
29 changes: 0 additions & 29 deletions sqlmesh/lsp/description.py

This file was deleted.

16 changes: 10 additions & 6 deletions sqlmesh/lsp/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
ApiResponseGetLineage,
ApiResponseGetModels,
)

# Define the command constant
EXTERNAL_MODEL_UPDATE_COLUMNS = "sqlmesh.external_model_update_columns"

from sqlmesh.lsp.completions import get_sql_completions
from sqlmesh.lsp.context import (
LSPContext,
Expand Down Expand Up @@ -60,15 +64,15 @@
from sqlmesh.lsp.helpers import to_lsp_range, to_sqlmesh_position
from sqlmesh.lsp.hints import get_hints
from sqlmesh.lsp.reference import (
LSPCteReference,
LSPModelReference,
LSPExternalModelReference,
CTEReference,
ModelReference,
get_references,
get_all_references,
)
from sqlmesh.lsp.rename import prepare_rename, rename_symbol, get_document_highlights
from sqlmesh.lsp.uri import URI
from sqlmesh.utils.errors import ConfigError
from sqlmesh.utils.lineage import ExternalModelReference
from web.server.api.endpoints.lineage import column_lineage, model_lineage
from web.server.api.endpoints.models import get_models
from typing import Union
Expand Down Expand Up @@ -479,7 +483,7 @@ def hover(ls: LanguageServer, params: types.HoverParams) -> t.Optional[types.Hov
if not references:
return None
reference = references[0]
if isinstance(reference, LSPCteReference) or not reference.markdown_description:
if isinstance(reference, CTEReference) or not reference.markdown_description:
return None
return types.Hover(
contents=types.MarkupContent(
Expand Down Expand Up @@ -525,7 +529,7 @@ def goto_definition(
location_links = []
for reference in references:
# Use target_range if available (CTEs, Macros, and external models in YAML)
if isinstance(reference, LSPModelReference):
if isinstance(reference, ModelReference):
# Regular SQL models - default to start of file
target_range = types.Range(
start=types.Position(line=0, character=0),
Expand All @@ -535,7 +539,7 @@ def goto_definition(
start=types.Position(line=0, character=0),
end=types.Position(line=0, character=0),
)
elif isinstance(reference, LSPExternalModelReference):
elif isinstance(reference, ExternalModelReference):
# External models may have target_range set for YAML files
target_range = types.Range(
start=types.Position(line=0, character=0),
Expand Down
Loading
Loading