Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Display LSP diagnostics in console #1385

Draft
wants to merge 8 commits into
base: feature/multiplexer/refactor-transpile-error
Choose a base branch
from
Draft
5 changes: 4 additions & 1 deletion src/databricks/labs/remorph/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,10 @@ def transpile(
sdk_config=sdk_config,
)

status = do_transpile(ctx.workspace_client, engine, config)
status, errors = do_transpile(ctx.workspace_client, engine, config)

for error in errors:
print(str(error))

print(json.dumps(status))

Expand Down
7 changes: 5 additions & 2 deletions src/databricks/labs/remorph/transpiler/execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import logging
import os
from pathlib import Path
from typing import Any

from databricks.labs.remorph.__about__ import __version__
from databricks.labs.remorph.config import (
Expand Down Expand Up @@ -141,7 +142,9 @@ def _process_input_file(


@timeit
def transpile(workspace_client: WorkspaceClient, engine: TranspileEngine, config: TranspileConfig):
def transpile(
workspace_client: WorkspaceClient, engine: TranspileEngine, config: TranspileConfig
) -> tuple[list[dict[str, Any]], list[TranspileError]]:
"""
[Experimental] Transpiles the SQL queries from one dialect to another.

Expand Down Expand Up @@ -191,7 +194,7 @@ def transpile(workspace_client: WorkspaceClient, engine: TranspileEngine, config
"error_log_file": str(error_log_file),
}
)
return status
return status, result.error_list


def verify_workspace_client(workspace_client: WorkspaceClient) -> WorkspaceClient:
Expand Down
53 changes: 52 additions & 1 deletion src/databricks/labs/remorph/transpiler/lsp/lsp_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@
TextDocumentIdentifier,
METHOD_TO_TYPES,
LanguageKind,
Range as LSPRange,
Position as LSPPosition,
_SPECIAL_PROPERTIES,
DiagnosticSeverity,
)
from pygls.lsp.client import BaseLanguageClient
from pygls.exceptions import FeatureRequestError
Expand All @@ -40,6 +43,13 @@
from databricks.labs.remorph.config import TranspileConfig, TranspileResult
from databricks.labs.remorph.errors.exceptions import IllegalStateException
from databricks.labs.remorph.transpiler.transpile_engine import TranspileEngine
from databricks.labs.remorph.transpiler.transpile_status import (
TranspileError,
ErrorKind,
ErrorSeverity,
CodeRange,
CodePosition,
)

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -231,6 +241,46 @@ def _apply(cls, lines: list[str], change: TextEdit) -> list[str]:
return result


class DiagnosticConverter(abc.ABC):

_KIND_NAMES = {e.name for e in ErrorKind}

@classmethod
def apply(cls, file_path: Path, diagnostic: Diagnostic) -> TranspileError:
code = str(diagnostic.code)
kind = ErrorKind.INTERNAL
parts = code.split("-")
if len(parts) >= 2 and parts[0] in cls._KIND_NAMES:
kind = ErrorKind[parts[0]]
parts.pop(0)
code = "-".join(parts)
severity = cls._convert_severity(diagnostic.severity)
lsp_range = cls._convert_range(diagnostic.range)
return TranspileError(
code=code, kind=kind, severity=severity, path=file_path, message=diagnostic.message, range=lsp_range
)

@classmethod
def _convert_range(cls, lsp_range: LSPRange | None) -> CodeRange | None:
if not lsp_range:
return None
return CodeRange(cls._convert_position(lsp_range.start), cls._convert_position(lsp_range.end))

@classmethod
def _convert_position(cls, lsp_position: LSPPosition) -> CodePosition:
return CodePosition(lsp_position.line, lsp_position.character)

@classmethod
def _convert_severity(cls, severity: DiagnosticSeverity | None) -> ErrorSeverity:
if severity == DiagnosticSeverity.Information:
return ErrorSeverity.INFO
if severity == DiagnosticSeverity.Warning:
return ErrorSeverity.WARNING
if severity == DiagnosticSeverity.Error:
return ErrorSeverity.ERROR
return ErrorSeverity.INFO


class LSPEngine(TranspileEngine):

@classmethod
Expand Down Expand Up @@ -320,7 +370,8 @@ async def transpile(
response = await self.transpile_document(file_path)
self.close_document(file_path)
transpiled_code = ChangeManager.apply(source_code, response.changes)
return TranspileResult(transpiled_code, 1, [])
transpile_errors = [DiagnosticConverter.apply(file_path, diagnostic) for diagnostic in response.diagnostics]
return TranspileResult(transpiled_code, 1, transpile_errors)

def analyse_table_lineage(
self, source_dialect: str, source_code: str, file_path: Path
Expand Down
1 change: 1 addition & 0 deletions tests/resources/lsp_transpiler/internal.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
create table stuff(name varchar(12))
41 changes: 34 additions & 7 deletions tests/resources/lsp_transpiler/lsp_server.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import os
import sys
from collections.abc import Sequence
from pathlib import Path
from typing import Any, Literal
from uuid import uuid4

Expand All @@ -23,6 +24,7 @@
Position,
METHOD_TO_TYPES,
_SPECIAL_PROPERTIES,
DiagnosticSeverity,
)
from pygls.lsp.server import LanguageServer

Expand Down Expand Up @@ -112,14 +114,39 @@ async def did_initialize(self, init_params: InitializeParams) -> None:
def transpile_to_databricks(self, params: TranspileDocumentParams) -> TranspileDocumentResult:
source_sql = self.workspace.get_text_document(params.uri).source
source_lines = source_sql.split("\n")
transpiled_sql = source_sql.upper()
changes = [
TextEdit(
range=Range(start=Position(0, 0), end=Position(len(source_lines), len(source_lines[-1]))),
new_text=transpiled_sql,
range = Range(start=Position(0, 0), end=Position(len(source_lines), len(source_lines[-1])))
transpiled_sql, diagnostics = self._transpile(Path(params.uri).name, range, source_sql)
changes = [TextEdit(range=range, new_text=transpiled_sql)]
return TranspileDocumentResult(uri=params.uri, changes=changes, diagnostics=diagnostics)

def _transpile(self, file_name: str, lsp_range: Range, source_sql: str) -> tuple[str, list[Diagnostic]]:
if file_name == "no_transpile.sql":
diagnostic = Diagnostic(
range=lsp_range,
message="No transpilation required",
severity=DiagnosticSeverity.Information,
code="GENERATION-NOT_REQUIRED",
)
]
return TranspileDocumentResult(uri=params.uri, changes=changes, diagnostics=[])
return source_sql, [diagnostic]
elif file_name == "unsupported_lca.sql":
diagnostic = Diagnostic(
range=lsp_range,
message="LCA conversion not supported",
severity=DiagnosticSeverity.Error,
code="ANALYSIS-UNSUPPORTED_LCA",
)
return source_sql, [diagnostic]
elif file_name == "internal.sql":
diagnostic = Diagnostic(
range=lsp_range,
message="Something went wrong",
severity=DiagnosticSeverity.Warning,
code="SOME_ERROR_CODE",
)
return source_sql, [diagnostic]
else:
# general test case
return source_sql.upper(), []


server = TestLspServer("test-lsp-server", "v0.1")
Expand Down
1 change: 1 addition & 0 deletions tests/resources/lsp_transpiler/no_transpile.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
create table stuff(name varchar(12))
1 change: 1 addition & 0 deletions tests/resources/lsp_transpiler/unsupported_lca.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
create table stuff(name varchar(12))
51 changes: 46 additions & 5 deletions tests/unit/test_cli_transpile.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from databricks.sdk import WorkspaceClient

from databricks.labs.remorph.transpiler.transpile_engine import TranspileEngine
from tests.unit.conftest import path_to_resource


def test_transpile_with_missing_installation():
Expand Down Expand Up @@ -35,7 +36,7 @@ def test_transpile_with_no_sdk_config():
workspace_client = create_autospec(WorkspaceClient)
with (
patch("databricks.labs.remorph.cli.ApplicationContext", autospec=True) as mock_app_context,
patch("databricks.labs.remorph.cli.do_transpile", return_value={}) as mock_transpile,
patch("databricks.labs.remorph.cli.do_transpile", return_value=({}, [])) as mock_transpile,
patch("os.path.exists", return_value=True),
):
default_config = TranspileConfig(
Expand Down Expand Up @@ -84,7 +85,7 @@ def test_transpile_with_warehouse_id_in_sdk_config():
with (
patch("databricks.labs.remorph.cli.ApplicationContext", autospec=True) as mock_app_context,
patch("os.path.exists", return_value=True),
patch("databricks.labs.remorph.cli.do_transpile", return_value={}) as mock_transpile,
patch("databricks.labs.remorph.cli.do_transpile", return_value=({}, [])) as mock_transpile,
):
sdk_config = {"warehouse_id": "w_id"}
default_config = TranspileConfig(
Expand Down Expand Up @@ -133,7 +134,7 @@ def test_transpile_with_cluster_id_in_sdk_config():
with (
patch("databricks.labs.remorph.cli.ApplicationContext", autospec=True) as mock_app_context,
patch("os.path.exists", return_value=True),
patch("databricks.labs.remorph.cli.do_transpile", return_value={}) as mock_transpile,
patch("databricks.labs.remorph.cli.do_transpile", return_value=({}, [])) as mock_transpile,
):
sdk_config = {"cluster_id": "c_id"}
default_config = TranspileConfig(
Expand Down Expand Up @@ -278,7 +279,7 @@ def test_transpile_with_valid_input(mock_workspace_client_cli):

with (
patch("os.path.exists", return_value=True),
patch("databricks.labs.remorph.cli.do_transpile", return_value={}) as mock_transpile,
patch("databricks.labs.remorph.cli.do_transpile", return_value=({}, [])) as mock_transpile,
):
cli.transpile(
mock_workspace_client_cli,
Expand Down Expand Up @@ -308,6 +309,46 @@ def test_transpile_with_valid_input(mock_workspace_client_cli):
)


def test_transpile_with_valid_transpiler(mock_workspace_client_cli):
transpiler_config_path = path_to_resource("lsp_transpiler", "lsp_config.yml")
source_dialect = "snowflake"
input_source = path_to_resource("functional", "snowflake", "aggregates", "least_1.sql")
output_folder = path_to_resource("lsp_transpiler")
skip_validation = "true"
catalog_name = "my_catalog"
schema_name = "my_schema"
mode = "current"
sdk_config = {'cluster_id': 'test_cluster'}

with (patch("databricks.labs.remorph.cli.do_transpile", return_value=({}, [])) as mock_transpile,):
cli.transpile(
mock_workspace_client_cli,
transpiler_config_path,
source_dialect,
input_source,
output_folder,
skip_validation,
catalog_name,
schema_name,
mode,
)
mock_transpile.assert_called_once_with(
mock_workspace_client_cli,
ANY,
TranspileConfig(
transpiler_config_path=transpiler_config_path,
source_dialect=source_dialect,
input_source=input_source,
output_folder=output_folder,
sdk_config=sdk_config,
skip_validation=True,
catalog_name=catalog_name,
schema_name=schema_name,
mode=mode,
),
)


def test_transpile_empty_output_folder(mock_workspace_client_cli):
transpiler = "sqlglot"
source_dialect = "snowflake"
Expand All @@ -322,7 +363,7 @@ def test_transpile_empty_output_folder(mock_workspace_client_cli):

with (
patch("os.path.exists", return_value=True),
patch("databricks.labs.remorph.cli.do_transpile", return_value={}) as mock_transpile,
patch("databricks.labs.remorph.cli.do_transpile", return_value=({}, [])) as mock_transpile,
):
cli.transpile(
mock_workspace_client_cli,
Expand Down
16 changes: 8 additions & 8 deletions tests/unit/transpiler/test_execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def test_with_dir_skip_validation(initial_setup, mock_workspace_client):

# call transpile
with patch('databricks.labs.remorph.helpers.db_sql.get_sql_backend', return_value=MockBackend()):
status = transpile(mock_workspace_client, SqlglotEngine(), config)
status, _errors = transpile(mock_workspace_client, SqlglotEngine(), config)
# assert the status
assert status is not None, "Status returned by morph function is None"
assert isinstance(status, list), "Status returned by morph function is not a list"
Expand Down Expand Up @@ -222,7 +222,7 @@ def test_with_dir_with_output_folder_skip_validation(initial_setup, mock_workspa
skip_validation=True,
)
with patch('databricks.labs.remorph.helpers.db_sql.get_sql_backend', return_value=MockBackend()):
status = transpile(mock_workspace_client, SqlglotEngine(), config)
status, _errors = transpile(mock_workspace_client, SqlglotEngine(), config)
# assert the status
assert status is not None, "Status returned by morph function is None"
assert isinstance(status, list), "Status returned by morph function is not a list"
Expand Down Expand Up @@ -295,7 +295,7 @@ def test_with_file(initial_setup, mock_workspace_client):
),
patch("databricks.labs.remorph.transpiler.execute.Validator", return_value=mock_validate),
):
status = transpile(mock_workspace_client, SqlglotEngine(), config)
status, _errors = transpile(mock_workspace_client, SqlglotEngine(), config)

# assert the status
assert status is not None, "Status returned by transpile function is None"
Expand Down Expand Up @@ -344,7 +344,7 @@ def test_with_file_with_output_folder_skip_validation(initial_setup, mock_worksp
'databricks.labs.remorph.helpers.db_sql.get_sql_backend',
return_value=MockBackend(),
):
status = transpile(mock_workspace_client, SqlglotEngine(), config)
status, _errors = transpile(mock_workspace_client, SqlglotEngine(), config)

# assert the status
assert status is not None, "Status returned by morph function is None"
Expand Down Expand Up @@ -379,7 +379,7 @@ def test_with_not_a_sql_file_skip_validation(initial_setup, mock_workspace_clien
'databricks.labs.remorph.helpers.db_sql.get_sql_backend',
return_value=MockBackend(),
):
status = transpile(mock_workspace_client, SqlglotEngine(), config)
status, _errors = transpile(mock_workspace_client, SqlglotEngine(), config)

# assert the status
assert status is not None, "Status returned by transpile function is None"
Expand Down Expand Up @@ -497,7 +497,7 @@ def test_with_file_with_success(initial_setup, mock_workspace_client):
),
patch("databricks.labs.remorph.transpiler.execute.Validator", return_value=mock_validate),
):
status = transpile(mock_workspace_client, SqlglotEngine(), config)
status, _errors = transpile(mock_workspace_client, SqlglotEngine(), config)
# assert the status
assert status is not None, "Status returned by morph function is None"
assert isinstance(status, list), "Status returned by morph function is not a list"
Expand Down Expand Up @@ -540,7 +540,7 @@ def test_parse_error_handling(initial_setup, mock_workspace_client):
)

with patch('databricks.labs.remorph.helpers.db_sql.get_sql_backend', return_value=MockBackend()):
status = transpile(mock_workspace_client, SqlglotEngine(), config)
status, _errors = transpile(mock_workspace_client, SqlglotEngine(), config)

# assert the status
assert status is not None, "Status returned by morph function is None"
Expand Down Expand Up @@ -597,7 +597,7 @@ def test_token_error_handling(initial_setup, mock_workspace_client):
)

with patch('databricks.labs.remorph.helpers.db_sql.get_sql_backend', return_value=MockBackend()):
status = transpile(mock_workspace_client, SqlglotEngine(), config)
status, _errors = transpile(mock_workspace_client, SqlglotEngine(), config)
# assert the status
assert status is not None, "Status returned by morph function is None"
assert isinstance(status, list), "Status returned by morph function is not a list"
Expand Down
Loading
Loading