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
21 changes: 13 additions & 8 deletions sqlmesh/lsp/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,14 +496,15 @@ def goto_definition(
target_range = reference.target_range
target_selection_range = reference.target_range

location_links.append(
types.LocationLink(
target_uri=reference.uri,
target_selection_range=target_selection_range,
target_range=target_range,
origin_selection_range=reference.range,
if reference.uri is not None:
location_links.append(
types.LocationLink(
target_uri=reference.uri,
target_selection_range=target_selection_range,
target_range=target_range,
origin_selection_range=reference.range,
)
)
)
return location_links
except Exception as e:
ls.show_message(f"Error getting references: {e}", types.MessageType.Error)
Expand All @@ -521,7 +522,11 @@ def find_references(
all_references = get_all_references(context, uri, params.position)

# Convert references to Location objects
locations = [types.Location(uri=ref.uri, range=ref.range) for ref in all_references]
locations = [
types.Location(uri=ref.uri, range=ref.range)
for ref in all_references
if ref.uri is not None
]

return locations if locations else None
except Exception as e:
Expand Down
22 changes: 19 additions & 3 deletions sqlmesh/lsp/reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,13 @@ class LSPExternalModelReference(PydanticModel):
"""A LSP reference to an external model."""

type: t.Literal["external_model"] = "external_model"
uri: str
range: Range
markdown_description: t.Optional[str] = None
target_range: t.Optional[Range] = None
uri: t.Optional[str] = None
"""The URI of the external model, typically a YAML file, it is optional because
external models can be unregistered and so they URI is not available."""

markdown_description: t.Optional[str] = None


class LSPCteReference(PydanticModel):
Expand Down Expand Up @@ -224,7 +227,7 @@ def get_model_definitions_for_a_path(
references.extend(column_references)
continue

# For non-CTE tables, process as before (external model references)
# For non-CTE tables, process these as before (external model references)
# Normalize the table reference
unaliased = table.copy()
if unaliased.args.get("alias") is not None:
Expand All @@ -247,6 +250,19 @@ def get_model_definitions_for_a_path(
model_or_snapshot=normalized_reference_name, raise_if_missing=False
)
if referenced_model is None:
table_meta = TokenPositionDetails.from_meta(table.this.meta)
table_range_sqlmesh = table_meta.to_range(read_file)
start_pos_sqlmesh = table_range_sqlmesh.start
end_pos_sqlmesh = table_range_sqlmesh.end
references.append(
LSPExternalModelReference(
range=Range(
start=to_lsp_position(start_pos_sqlmesh),
end=to_lsp_position(end_pos_sqlmesh),
),
markdown_description="Unregistered external model",
)
)
continue
referenced_model_path = referenced_model._path
if referenced_model_path is None:
Expand Down
4 changes: 3 additions & 1 deletion tests/lsp/test_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ def test_reference() -> None:
references = get_model_definitions_for_a_path(lsp_context, active_customers_uri)

assert len(references) == 1
assert URI(references[0].uri) == URI.from_path(sushi_customers_path)
uri = references[0].uri
assert uri is not None
assert URI(uri) == URI.from_path(sushi_customers_path)

# Check that the reference in the correct range is sushi.customers
path = active_customers_uri.to_path()
Expand Down
32 changes: 30 additions & 2 deletions tests/lsp/test_reference_external_model.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
from pathlib import Path

from lsprotocol.types import Position

from sqlmesh import Config
from sqlmesh.core.context import Context
from sqlmesh.core.linter.helpers import read_range_from_file
from sqlmesh.lsp.context import LSPContext, ModelTarget
from sqlmesh.lsp.helpers import to_sqlmesh_range
from sqlmesh.lsp.reference import get_references, LSPExternalModelReference
from sqlmesh.lsp.uri import URI
from tests.utils.test_filesystem import create_temp_file


def test_reference() -> None:
Expand All @@ -25,14 +30,37 @@ def test_reference() -> None:
assert len(references) == 1
reference = references[0]
assert isinstance(reference, LSPExternalModelReference)
assert reference.uri.endswith("external_models.yaml")
uri = reference.uri
assert uri is not None
assert uri.endswith("external_models.yaml")

source_range = read_range_from_file(customers, to_sqlmesh_range(reference.range))
assert source_range == "raw.demographics"

if reference.target_range is None:
raise AssertionError("Reference target range should not be None")
uri = reference.uri
assert uri is not None
target_range = read_range_from_file(
URI(reference.uri).to_path(), to_sqlmesh_range(reference.target_range)
URI(uri).to_path(), to_sqlmesh_range(reference.target_range)
)
assert target_range == "raw.demographics"


def test_unregistered_external_model(tmp_path: Path):
model_path = tmp_path / "models" / "foo.sql"
contents = "MODEL (name test.foo, kind FULL); SELECT * FROM external_model"
create_temp_file(tmp_path, model_path, contents)
ctx = Context(paths=[tmp_path], config=Config())
lsp_context = LSPContext(ctx)

uri = URI.from_path(model_path)
references = get_references(lsp_context, uri, Position(line=0, character=len(contents) - 3))

assert len(references) == 1
reference = references[0]
assert isinstance(reference, LSPExternalModelReference)
assert reference.uri is None
assert reference.target_range is None
assert reference.markdown_description == "Unregistered external model"
assert read_range_from_file(model_path, to_sqlmesh_range(reference.range)) == "external_model"