Skip to content

Commit 185b9cc

Browse files
committed
feat(vscode): adding ability to update columns
1 parent 3ee4e9f commit 185b9cc

File tree

8 files changed

+376
-22
lines changed

8 files changed

+376
-22
lines changed

pnpm-lock.yaml

Lines changed: 61 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sqlmesh/core/schema_loader.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -57,28 +57,17 @@ def create_external_models_file(
5757
external_model_fqns -= existing_model_fqns
5858

5959
with ThreadPoolExecutor(max_workers=max_workers) as pool:
60-
61-
def _get_columns(table: str) -> t.Optional[t.Dict[str, t.Any]]:
62-
try:
63-
return adapter.columns(table, include_pseudo_columns=True)
64-
except Exception as e:
65-
msg = f"Unable to get schema for '{table}': '{e}'."
66-
if strict:
67-
raise SQLMeshError(msg) from e
68-
get_console().log_warning(msg)
69-
return None
70-
7160
gateway_part = {"gateway": gateway} if gateway else {}
7261

7362
schemas = [
7463
{
7564
"name": exp.to_table(table).sql(dialect=dialect),
76-
"columns": {c: dtype.sql(dialect=dialect) for c, dtype in columns.items()},
65+
"columns": columns,
7766
**gateway_part,
7867
}
7968
for table, columns in sorted(
8069
pool.map(
81-
lambda table: (table, _get_columns(table)),
70+
lambda table: (table, get_columns(adapter, dialect, table, strict)),
8271
external_model_fqns,
8372
)
8473
)
@@ -94,3 +83,20 @@ def _get_columns(table: str) -> t.Optional[t.Dict[str, t.Any]]:
9483

9584
with open(path, "w", encoding="utf-8") as file:
9685
yaml.dump(entries_to_keep + schemas, file)
86+
87+
88+
def get_columns(
89+
adapter: EngineAdapter, dialect: DialectType, table: str, strict: bool
90+
) -> t.Optional[t.Dict[str, t.Any]]:
91+
"""
92+
Return the column and their types in a dictionary
93+
"""
94+
try:
95+
columns = adapter.columns(table, include_pseudo_columns=True)
96+
return {c: dtype.sql(dialect=dialect) for c, dtype in columns.items()}
97+
except Exception as e:
98+
msg = f"Unable to get schema for '{table}': '{e}'."
99+
if strict:
100+
raise SQLMeshError(msg) from e
101+
get_console().log_warning(msg)
102+
return None

sqlmesh/lsp/commands.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
EXTERNAL_MODEL_UPDATE_COLUMNS = "sqlmesh.external_model_update_columns"

sqlmesh/lsp/context.py

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,23 @@
11
from dataclasses import dataclass
22
from pathlib import Path
3+
4+
from pygls.server import LanguageServer
5+
36
from sqlmesh.core.context import Context
47
import typing as t
58

6-
from sqlmesh.core.model.definition import SqlModel
9+
from sqlmesh.core.model.definition import SqlModel, ExternalModel
710
from sqlmesh.core.linter.definition import AnnotatedRuleViolation
11+
from sqlmesh.core.schema_loader import get_columns
12+
from sqlmesh.lsp.commands import EXTERNAL_MODEL_UPDATE_COLUMNS
813
from sqlmesh.lsp.custom import ModelForRendering
914
from sqlmesh.lsp.custom import AllModelsResponse, RenderModelEntry
15+
from sqlmesh.lsp.helpers import to_lsp_range
1016
from sqlmesh.lsp.uri import URI
1117
from lsprotocol import types
18+
from pathlib import Path
19+
20+
from sqlmesh.utils import yaml
1221

1322

1423
@dataclass
@@ -228,9 +237,44 @@ def get_code_actions(
228237
edit=types.WorkspaceEdit(changes={params.text_document.uri: text_edits}),
229238
)
230239
code_actions.append(code_action)
231-
232240
return code_actions if code_actions else None
233241

242+
def get_code_lenses(self, uri: URI) -> t.Optional[t.List[types.CodeLens]]:
243+
models_in_file = self.map.get(uri.to_path())
244+
if isinstance(models_in_file, ModelTarget):
245+
models = [self.context.get_model(model) for model in models_in_file.names]
246+
if any(isinstance(model, ExternalModel) for model in models):
247+
code_lenses = self._get_external_model_code_lenses(uri)
248+
if code_lenses:
249+
return code_lenses
250+
251+
return None
252+
253+
def _get_external_model_code_lenses(self, uri: URI) -> t.List[types.CodeLens]:
254+
from sqlmesh.lsp.reference import get_yaml_model_name_ranges
255+
256+
"""Get code actions for external models YAML files."""
257+
ranges = get_yaml_model_name_ranges(uri.to_path())
258+
259+
code_lenses: t.List[types.CodeLens] = []
260+
for name, range in ranges.items():
261+
# Create a code action to update columns for external models
262+
command = types.Command(
263+
title="Update Columns",
264+
command=EXTERNAL_MODEL_UPDATE_COLUMNS,
265+
arguments=[
266+
name,
267+
],
268+
)
269+
code_lenses.append(
270+
types.CodeLens(
271+
range=to_lsp_range(range),
272+
command=command,
273+
)
274+
)
275+
276+
return code_lenses if code_lenses else []
277+
234278
def list_of_models_for_rendering(self) -> t.List[ModelForRendering]:
235279
"""Get a list of models for rendering.
236280
@@ -332,3 +376,73 @@ def diagnostic_to_lsp_diagnostic(
332376
code=diagnostic.rule.name,
333377
code_description=types.CodeDescription(href=rule_uri),
334378
)
379+
380+
def update_external_model_columns(self, ls: LanguageServer, uri: URI, model_name: str) -> bool:
381+
"""
382+
Update the columns for an external model in the YAML file. Returns True if changed, False if didn't because
383+
of the columns already being up to date.
384+
385+
Errors still throw exceptions to be handled by the caller.
386+
"""
387+
models = yaml.load(uri.to_path())
388+
if not isinstance(models, list):
389+
raise ValueError(
390+
f"Expected a list of models in {uri.to_path()}, but got {type(models).__name__}"
391+
)
392+
393+
existing_model = next((model for model in models if model.get("name") == model_name), None)
394+
if existing_model is None:
395+
raise ValueError(f"Could not find model {model_name} in {uri.to_path()}")
396+
397+
existing_model_columns = existing_model.get("columns")
398+
399+
# Get the adapter and fetch columns
400+
adapter = self.context.engine_adapter
401+
# Get columns for the model
402+
new_columns = get_columns(
403+
adapter=adapter,
404+
dialect=self.context.config.model_defaults.dialect,
405+
table=model_name,
406+
strict=True,
407+
)
408+
# Compare existing columns and matching types and if they are the same, do not update
409+
if existing_model_columns is not None:
410+
if existing_model_columns == new_columns:
411+
ls.show_message("Columns already up to date")
412+
return False
413+
414+
# Model index to update
415+
model_index = next(
416+
(i for i, model in enumerate(models) if model.get("name") == model_name), None
417+
)
418+
if model_index is None:
419+
raise ValueError(f"Could not find model {model_name} in {uri.to_path()}")
420+
421+
# Get end of the file to set the edit range
422+
with open(uri.to_path(), "r", encoding="utf-8") as file:
423+
read_file = file.read()
424+
425+
end_line = read_file.count("\n")
426+
end_character = len(read_file.splitlines()[-1]) if end_line > 0 else 0
427+
428+
models[model_index]["columns"] = new_columns
429+
edit = types.TextDocumentEdit(
430+
text_document=types.OptionalVersionedTextDocumentIdentifier(
431+
uri=uri.value,
432+
version=None,
433+
),
434+
edits=[
435+
types.TextEdit(
436+
range=types.Range(
437+
start=types.Position(line=0, character=0),
438+
end=types.Position(
439+
line=end_line,
440+
character=end_character,
441+
),
442+
),
443+
new_text=yaml.dump(models),
444+
)
445+
],
446+
)
447+
ls.apply_edit(types.WorkspaceEdit(document_changes=[edit]))
448+
return True

sqlmesh/lsp/main.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
ApiResponseGetLineage,
2424
ApiResponseGetModels,
2525
)
26+
from sqlmesh.lsp.commands import EXTERNAL_MODEL_UPDATE_COLUMNS
2627
from sqlmesh.lsp.completions import get_sql_completions
2728
from sqlmesh.lsp.context import (
2829
LSPContext,
@@ -311,6 +312,44 @@ def function_call(ls: LanguageServer, params: t.Any) -> t.Dict[str, t.Any]:
311312

312313
self.server.feature(name)(create_function_call(method))
313314

315+
@self.server.command(EXTERNAL_MODEL_UPDATE_COLUMNS)
316+
def command_external_models_update_columns(ls: LanguageServer, raw: t.Any) -> None:
317+
try:
318+
if not isinstance(raw, list):
319+
raise ValueError("Invalid command parameters")
320+
if len(raw) != 1:
321+
raise ValueError("Command expects exactly one parameter")
322+
model_name = raw[0]
323+
if not isinstance(model_name, str):
324+
raise ValueError("Command parameter must be a string")
325+
326+
context = self._context_get_or_load()
327+
if not isinstance(context, LSPContext):
328+
raise ValueError("Context is not loaded or invalid")
329+
model = context.context.get_model(model_name)
330+
if model is None:
331+
raise ValueError(f"External model '{model_name}' not found")
332+
if model._path is None:
333+
raise ValueError(f"External model '{model_name}' does not have a file path")
334+
uri = URI.from_path(model._path)
335+
updated = context.update_external_model_columns(
336+
ls=ls,
337+
uri=uri,
338+
model_name=model_name,
339+
)
340+
if updated:
341+
ls.show_message(
342+
f"Updated columns for '{model_name}'",
343+
types.MessageType.Info,
344+
)
345+
else:
346+
ls.show_message(
347+
f"Columns for '{model_name}' are already up to date",
348+
)
349+
except Exception as e:
350+
ls.show_message(f"Error executing command: {e}", types.MessageType.Error)
351+
return None
352+
314353
@self.server.feature(types.INITIALIZE)
315354
def initialize(ls: LanguageServer, params: types.InitializeParams) -> None:
316355
"""Initialize the server when the client connects."""
@@ -693,6 +732,17 @@ def code_action(
693732
ls.log_trace(f"Error getting code actions: {e}")
694733
return None
695734

735+
@self.server.feature(types.TEXT_DOCUMENT_CODE_LENS)
736+
def code_lens(ls: LanguageServer, params: types.CodeLensParams) -> t.List[types.CodeLens]:
737+
try:
738+
uri = URI(params.text_document.uri)
739+
context = self._context_get_or_load(uri)
740+
code_lenses = context.get_code_lenses(uri)
741+
return code_lenses if code_lenses else []
742+
except Exception as e:
743+
ls.log_trace(f"Error getting code lenses: {e}")
744+
return []
745+
696746
@self.server.feature(
697747
types.TEXT_DOCUMENT_COMPLETION,
698748
types.CompletionOptions(trigger_characters=["@"]), # advertise "@" for macros

0 commit comments

Comments
 (0)