From 9949faf2afec85c61abd958ce896ffac0ee94f86 Mon Sep 17 00:00:00 2001 From: Ben King <9087625+benfdking@users.noreply.github.com> Date: Fri, 25 Jul 2025 15:38:06 +0100 Subject: [PATCH] feat(lsp): references function now detects non registered external models --- sqlmesh/lsp/main.py | 21 ++++++++------ sqlmesh/lsp/reference.py | 22 +++++++++++++-- tests/lsp/test_reference.py | 4 ++- tests/lsp/test_reference_external_model.py | 32 ++++++++++++++++++++-- 4 files changed, 65 insertions(+), 14 deletions(-) diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index b02e810997..dc633a3949 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -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) @@ -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: diff --git a/sqlmesh/lsp/reference.py b/sqlmesh/lsp/reference.py index 9f1215d9ca..342cd86893 100644 --- a/sqlmesh/lsp/reference.py +++ b/sqlmesh/lsp/reference.py @@ -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): @@ -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: @@ -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: diff --git a/tests/lsp/test_reference.py b/tests/lsp/test_reference.py index f39bddc059..736b995410 100644 --- a/tests/lsp/test_reference.py +++ b/tests/lsp/test_reference.py @@ -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() diff --git a/tests/lsp/test_reference_external_model.py b/tests/lsp/test_reference_external_model.py index ebf6420934..43c873991e 100644 --- a/tests/lsp/test_reference_external_model.py +++ b/tests/lsp/test_reference_external_model.py @@ -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: @@ -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"