sqlmesh 0.196.3.dev4__py3-none-any.whl → 0.196.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sqlmesh/_version.py +2 -2
- sqlmesh/cli/main.py +15 -6
- sqlmesh/core/engine_adapter/mssql.py +85 -2
- sqlmesh/core/linter/definition.py +5 -2
- sqlmesh/core/linter/rule.py +27 -2
- sqlmesh/core/linter/rules/builtin.py +25 -2
- sqlmesh/core/model/definition.py +1 -0
- sqlmesh/core/state_sync/db/environment.py +18 -14
- sqlmesh/core/state_sync/db/facade.py +0 -4
- sqlmesh/lsp/context.py +158 -0
- sqlmesh/lsp/main.py +95 -120
- {sqlmesh-0.196.3.dev4.dist-info → sqlmesh-0.196.4.dist-info}/METADATA +2 -2
- {sqlmesh-0.196.3.dev4.dist-info → sqlmesh-0.196.4.dist-info}/RECORD +60 -60
- web/client/dist/assets/{Audits-DrNQaTQA.js → Audits-DKgXravk.js} +1 -1
- web/client/dist/assets/{Banner-DsG8aGIL.js → Banner-BlNaPns_.js} +1 -1
- web/client/dist/assets/{ChevronDownIcon-CpQjfO01.js → ChevronDownIcon-MZBJpOO8.js} +1 -1
- web/client/dist/assets/{ChevronRightIcon-B-9lnqGr.js → ChevronRightIcon-Bdxx50QS.js} +1 -1
- web/client/dist/assets/{Content-CSMzf1cU.js → Content-ClpOw61F.js} +1 -1
- web/client/dist/assets/{Content-vfltDlhj.js → Content-DRsp6uQI.js} +1 -1
- web/client/dist/assets/{Data-k1Dz6qdS.js → Data-91WCokZa.js} +1 -1
- web/client/dist/assets/{DataCatalog-DlbWZyBp.js → DataCatalog-C63qcucX.js} +2 -2
- web/client/dist/assets/{Editor--iBTA9xH.js → Editor-DRDwxpI2.js} +1 -1
- web/client/dist/assets/{Editor-C2pcRDlN.js → Editor-PtL9tfk8.js} +1 -1
- web/client/dist/assets/{Errors-CQW3lhsM.js → Errors-C-Webrjw.js} +1 -1
- web/client/dist/assets/{FileExplorer-IAIPoTxM.js → FileExplorer-ylHWQwsm.js} +1 -1
- web/client/dist/assets/{Footer-D77vCxKH.js → Footer-CasDkGte.js} +1 -1
- web/client/dist/assets/{Header-Ccb81kaP.js → Header-DyL7LEVP.js} +1 -1
- web/client/dist/assets/{Input-D_Fp4W_B.js → Input-DZamwNGa.js} +1 -1
- web/client/dist/assets/Lineage-BPGCodgf.js +1 -0
- web/client/dist/assets/{ListboxShow-BIPQ-cqE.js → ListboxShow-DSFZEE-M.js} +1 -1
- web/client/dist/assets/{ModelLineage-BrTZ2lm3.js → ModelLineage-BQmZD--B.js} +1 -1
- web/client/dist/assets/{Models-B6cHeIlA.js → Models-C2AYU-74.js} +1 -1
- web/client/dist/assets/{Page-S1ykkm5T.js → Page-BlOb3Ql3.js} +1 -1
- web/client/dist/assets/{Plan-DCD4m0nD.js → Plan-DWeacCNs.js} +1 -1
- web/client/dist/assets/{PlusCircleIcon-C40sAC-m.js → PlusCircleIcon-DD7nOhwQ.js} +1 -1
- web/client/dist/assets/{ReportErrors-CxqgDcYY.js → ReportErrors-CH5Vf5lv.js} +1 -1
- web/client/dist/assets/{Root-DtUrFfE_.js → Root-DZ2xKltj.js} +1 -1
- web/client/dist/assets/{SearchList-DoRflby9.js → SearchList-BpsOOoGh.js} +6 -6
- web/client/dist/assets/{SelectEnvironment-equ86mF1.js → SelectEnvironment-DLsh1BMi.js} +1 -1
- web/client/dist/assets/{SourceList-DT-sBCAQ.js → SourceList-Dg2mc7aY.js} +1 -1
- web/client/dist/assets/{SourceListItem-MFS9vpg9.js → SourceListItem-2mO71O7W.js} +1 -1
- web/client/dist/assets/{SplitPane-D2cB3Qkv.js → SplitPane-BbVgMyQB.js} +1 -1
- web/client/dist/assets/{Tests-Duzk1K8j.js → Tests-LDkS-T5R.js} +1 -1
- web/client/dist/assets/{Welcome-BgAHQe0m.js → Welcome-C0UCzzcU.js} +1 -1
- web/client/dist/assets/context-CGAXyfSr.js +71 -0
- web/client/dist/assets/{context-g_ECqgnp.js → context-D4y_v88x.js} +1 -1
- web/client/dist/assets/{editor-BddPKujl.js → editor-BOTuvmku.js} +1 -1
- web/client/dist/assets/{file-DUr_Gx7F.js → file-CXPqyzKt.js} +1 -1
- web/client/dist/assets/{floating-ui.react-dom-DTKAUIBI.js → floating-ui.react-dom-X8vTMPq7.js} +1 -1
- web/client/dist/assets/{help-LQUMs6nO.js → help-65lcCO6u.js} +1 -1
- web/client/dist/assets/{index-BsS6H8XQ.js → index-C7dOv_Xt.js} +8 -8
- web/client/dist/assets/{index-DvxjegO0.js → index-Cf0u2ogu.js} +1 -1
- web/client/dist/assets/{plan-DS237yuz.js → plan-D5w005YE.js} +1 -1
- web/client/dist/assets/{popover-CiiY17g7.js → popover-CQm70UjD.js} +1 -1
- web/client/dist/assets/{project-DUHOhQT2.js → project-CUisbOpA.js} +1 -1
- web/client/dist/index.html +1 -1
- web/client/dist/assets/Lineage-BHLfUAmC.js +0 -1
- web/client/dist/assets/context-CXV8eAaM.js +0 -68
- {sqlmesh-0.196.3.dev4.dist-info → sqlmesh-0.196.4.dist-info}/WHEEL +0 -0
- {sqlmesh-0.196.3.dev4.dist-info → sqlmesh-0.196.4.dist-info}/entry_points.txt +0 -0
- {sqlmesh-0.196.3.dev4.dist-info → sqlmesh-0.196.4.dist-info}/licenses/LICENSE +0 -0
- {sqlmesh-0.196.3.dev4.dist-info → sqlmesh-0.196.4.dist-info}/top_level.txt +0 -0
sqlmesh/_version.py
CHANGED
|
@@ -17,5 +17,5 @@ __version__: str
|
|
|
17
17
|
__version_tuple__: VERSION_TUPLE
|
|
18
18
|
version_tuple: VERSION_TUPLE
|
|
19
19
|
|
|
20
|
-
__version__ = version = '0.196.
|
|
21
|
-
__version_tuple__ = version_tuple = (0, 196,
|
|
20
|
+
__version__ = version = '0.196.4'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 196, 4)
|
sqlmesh/cli/main.py
CHANGED
|
@@ -4,25 +4,26 @@ import logging
|
|
|
4
4
|
import os
|
|
5
5
|
import sys
|
|
6
6
|
import typing as t
|
|
7
|
+
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
import click
|
|
10
|
+
|
|
9
11
|
from sqlmesh import configure_logging, remove_excess_logs
|
|
10
12
|
from sqlmesh.cli import error_handler
|
|
11
13
|
from sqlmesh.cli import options as opt
|
|
12
14
|
from sqlmesh.cli.project_init import (
|
|
15
|
+
InitCliMode,
|
|
13
16
|
ProjectTemplate,
|
|
14
17
|
init_example_project,
|
|
15
|
-
InitCliMode,
|
|
16
18
|
interactive_init,
|
|
17
19
|
)
|
|
18
20
|
from sqlmesh.core.analytics import cli_analytics
|
|
19
|
-
from sqlmesh.core.console import configure_console, get_console
|
|
20
|
-
from sqlmesh.utils import Verbosity
|
|
21
21
|
from sqlmesh.core.config import load_configs
|
|
22
|
+
from sqlmesh.core.console import configure_console, get_console
|
|
22
23
|
from sqlmesh.core.context import Context
|
|
24
|
+
from sqlmesh.utils import Verbosity
|
|
23
25
|
from sqlmesh.utils.date import TimeLike
|
|
24
26
|
from sqlmesh.utils.errors import MissingDependencyError, SQLMeshError
|
|
25
|
-
from pathlib import Path
|
|
26
27
|
|
|
27
28
|
logger = logging.getLogger(__name__)
|
|
28
29
|
|
|
@@ -355,6 +356,7 @@ def evaluate(
|
|
|
355
356
|
"--append-newline",
|
|
356
357
|
is_flag=True,
|
|
357
358
|
help="Include a newline at the end of each file.",
|
|
359
|
+
default=None,
|
|
358
360
|
)
|
|
359
361
|
@opt.format_options
|
|
360
362
|
@click.pass_context
|
|
@@ -805,7 +807,11 @@ def check_intervals(
|
|
|
805
807
|
context = ctx.obj
|
|
806
808
|
context.console.show_intervals(
|
|
807
809
|
context.check_intervals(
|
|
808
|
-
environment,
|
|
810
|
+
environment,
|
|
811
|
+
no_signals=no_signals,
|
|
812
|
+
select_models=select_model,
|
|
813
|
+
start=start,
|
|
814
|
+
end=end,
|
|
809
815
|
)
|
|
810
816
|
)
|
|
811
817
|
|
|
@@ -1107,7 +1113,10 @@ def clean(obj: Context) -> None:
|
|
|
1107
1113
|
@error_handler
|
|
1108
1114
|
@cli_analytics
|
|
1109
1115
|
def table_name(
|
|
1110
|
-
obj: Context,
|
|
1116
|
+
obj: Context,
|
|
1117
|
+
model_name: str,
|
|
1118
|
+
environment: t.Optional[str] = None,
|
|
1119
|
+
prod: bool = False,
|
|
1111
1120
|
) -> None:
|
|
1112
1121
|
"""Prints the name of the physical table for the given model."""
|
|
1113
1122
|
print(obj.table_name(model_name, environment, prod))
|
|
@@ -6,11 +6,13 @@ import typing as t
|
|
|
6
6
|
|
|
7
7
|
from sqlglot import exp
|
|
8
8
|
|
|
9
|
-
from sqlmesh.core.dialect import to_schema
|
|
9
|
+
from sqlmesh.core.dialect import to_schema, add_table
|
|
10
10
|
from sqlmesh.core.engine_adapter.base import (
|
|
11
11
|
EngineAdapterWithIndexSupport,
|
|
12
12
|
EngineAdapter,
|
|
13
13
|
InsertOverwriteStrategy,
|
|
14
|
+
MERGE_SOURCE_ALIAS,
|
|
15
|
+
MERGE_TARGET_ALIAS,
|
|
14
16
|
)
|
|
15
17
|
from sqlmesh.core.engine_adapter.mixins import (
|
|
16
18
|
GetCurrentCatalogFromFunctionMixin,
|
|
@@ -32,7 +34,7 @@ from sqlmesh.core.schema_diff import SchemaDiffer
|
|
|
32
34
|
|
|
33
35
|
if t.TYPE_CHECKING:
|
|
34
36
|
from sqlmesh.core._typing import SchemaName, TableName
|
|
35
|
-
from sqlmesh.core.engine_adapter._typing import DF, Query
|
|
37
|
+
from sqlmesh.core.engine_adapter._typing import DF, Query, QueryOrDF
|
|
36
38
|
|
|
37
39
|
|
|
38
40
|
@set_catalog()
|
|
@@ -188,6 +190,87 @@ class MSSQLEngineAdapter(
|
|
|
188
190
|
)
|
|
189
191
|
super().drop_schema(schema_name, ignore_if_not_exists=ignore_if_not_exists, cascade=False)
|
|
190
192
|
|
|
193
|
+
def merge(
|
|
194
|
+
self,
|
|
195
|
+
target_table: TableName,
|
|
196
|
+
source_table: QueryOrDF,
|
|
197
|
+
columns_to_types: t.Optional[t.Dict[str, exp.DataType]],
|
|
198
|
+
unique_key: t.Sequence[exp.Expression],
|
|
199
|
+
when_matched: t.Optional[exp.Whens] = None,
|
|
200
|
+
merge_filter: t.Optional[exp.Expression] = None,
|
|
201
|
+
) -> None:
|
|
202
|
+
source_queries, columns_to_types = self._get_source_queries_and_columns_to_types(
|
|
203
|
+
source_table, columns_to_types, target_table=target_table
|
|
204
|
+
)
|
|
205
|
+
columns_to_types = columns_to_types or self.columns(target_table)
|
|
206
|
+
on = exp.and_(
|
|
207
|
+
*(
|
|
208
|
+
add_table(part, MERGE_TARGET_ALIAS).eq(add_table(part, MERGE_SOURCE_ALIAS))
|
|
209
|
+
for part in unique_key
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
if merge_filter:
|
|
213
|
+
on = exp.and_(merge_filter, on)
|
|
214
|
+
|
|
215
|
+
if not when_matched:
|
|
216
|
+
match_condition = None
|
|
217
|
+
unique_key_names = [y.name for y in unique_key]
|
|
218
|
+
columns_to_types_no_keys = [c for c in columns_to_types if c not in unique_key_names]
|
|
219
|
+
|
|
220
|
+
target_columns_no_keys = [
|
|
221
|
+
exp.column(c, MERGE_TARGET_ALIAS) for c in columns_to_types_no_keys
|
|
222
|
+
]
|
|
223
|
+
source_columns_no_keys = [
|
|
224
|
+
exp.column(c, MERGE_SOURCE_ALIAS) for c in columns_to_types_no_keys
|
|
225
|
+
]
|
|
226
|
+
|
|
227
|
+
match_condition = exp.Exists(
|
|
228
|
+
this=exp.select(*target_columns_no_keys).except_(
|
|
229
|
+
exp.select(*source_columns_no_keys)
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
match_expressions = [
|
|
234
|
+
exp.When(
|
|
235
|
+
matched=True,
|
|
236
|
+
source=False,
|
|
237
|
+
condition=match_condition,
|
|
238
|
+
then=exp.Update(
|
|
239
|
+
expressions=[
|
|
240
|
+
exp.column(col, MERGE_TARGET_ALIAS).eq(
|
|
241
|
+
exp.column(col, MERGE_SOURCE_ALIAS)
|
|
242
|
+
)
|
|
243
|
+
for col in columns_to_types_no_keys
|
|
244
|
+
],
|
|
245
|
+
),
|
|
246
|
+
)
|
|
247
|
+
]
|
|
248
|
+
else:
|
|
249
|
+
match_expressions = when_matched.copy().expressions
|
|
250
|
+
|
|
251
|
+
match_expressions.append(
|
|
252
|
+
exp.When(
|
|
253
|
+
matched=False,
|
|
254
|
+
source=False,
|
|
255
|
+
then=exp.Insert(
|
|
256
|
+
this=exp.Tuple(expressions=[exp.column(col) for col in columns_to_types]),
|
|
257
|
+
expression=exp.Tuple(
|
|
258
|
+
expressions=[
|
|
259
|
+
exp.column(col, MERGE_SOURCE_ALIAS) for col in columns_to_types
|
|
260
|
+
]
|
|
261
|
+
),
|
|
262
|
+
),
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
for source_query in source_queries:
|
|
266
|
+
with source_query as query:
|
|
267
|
+
self._merge(
|
|
268
|
+
target_table=target_table,
|
|
269
|
+
query=query,
|
|
270
|
+
on=on,
|
|
271
|
+
whens=exp.Whens(expressions=match_expressions),
|
|
272
|
+
)
|
|
273
|
+
|
|
191
274
|
def _convert_df_datetime(self, df: DF, columns_to_types: t.Dict[str, exp.DataType]) -> None:
|
|
192
275
|
import pandas as pd
|
|
193
276
|
from pandas.api.types import is_datetime64_any_dtype # type: ignore
|
|
@@ -8,7 +8,7 @@ import operator as op
|
|
|
8
8
|
from collections.abc import Iterator, Iterable, Set, Mapping, Callable
|
|
9
9
|
from functools import reduce
|
|
10
10
|
from sqlmesh.core.model import Model
|
|
11
|
-
from sqlmesh.core.linter.rule import Rule, RuleViolation, Range
|
|
11
|
+
from sqlmesh.core.linter.rule import Rule, RuleViolation, Range, Fix
|
|
12
12
|
from sqlmesh.core.console import LinterConsole, get_console
|
|
13
13
|
|
|
14
14
|
if t.TYPE_CHECKING:
|
|
@@ -75,6 +75,7 @@ class Linter:
|
|
|
75
75
|
model=model,
|
|
76
76
|
violation_type="error",
|
|
77
77
|
violation_range=violation.violation_range,
|
|
78
|
+
fixes=violation.fixes,
|
|
78
79
|
)
|
|
79
80
|
for violation in error_violations
|
|
80
81
|
] + [
|
|
@@ -84,6 +85,7 @@ class Linter:
|
|
|
84
85
|
model=model,
|
|
85
86
|
violation_type="warning",
|
|
86
87
|
violation_range=violation.violation_range,
|
|
88
|
+
fixes=violation.fixes,
|
|
87
89
|
)
|
|
88
90
|
for violation in warn_violations
|
|
89
91
|
]
|
|
@@ -152,7 +154,8 @@ class AnnotatedRuleViolation(RuleViolation):
|
|
|
152
154
|
model: Model,
|
|
153
155
|
violation_type: t.Literal["error", "warning"],
|
|
154
156
|
violation_range: t.Optional[Range] = None,
|
|
157
|
+
fixes: t.Optional[t.List[Fix]] = None,
|
|
155
158
|
) -> None:
|
|
156
|
-
super().__init__(rule, violation_msg, violation_range)
|
|
159
|
+
super().__init__(rule, violation_msg, violation_range, fixes)
|
|
157
160
|
self.model = model
|
|
158
161
|
self.violation_type = violation_type
|
sqlmesh/core/linter/rule.py
CHANGED
|
@@ -39,6 +39,22 @@ class Range:
|
|
|
39
39
|
end: Position
|
|
40
40
|
|
|
41
41
|
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class TextEdit:
|
|
44
|
+
"""A text edit to apply to a file."""
|
|
45
|
+
|
|
46
|
+
range: Range
|
|
47
|
+
new_text: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True)
|
|
51
|
+
class Fix:
|
|
52
|
+
"""A fix that can be applied to resolve a rule violation."""
|
|
53
|
+
|
|
54
|
+
title: str
|
|
55
|
+
edits: t.List[TextEdit]
|
|
56
|
+
|
|
57
|
+
|
|
42
58
|
class _Rule(abc.ABCMeta):
|
|
43
59
|
def __new__(cls: Type[_Rule], clsname: str, bases: t.Tuple, attrs: t.Dict) -> _Rule:
|
|
44
60
|
attrs["name"] = clsname.lower()
|
|
@@ -66,10 +82,14 @@ class Rule(abc.ABC, metaclass=_Rule):
|
|
|
66
82
|
self,
|
|
67
83
|
violation_msg: t.Optional[str] = None,
|
|
68
84
|
violation_range: t.Optional[Range] = None,
|
|
85
|
+
fixes: t.Optional[t.List[Fix]] = None,
|
|
69
86
|
) -> RuleViolation:
|
|
70
87
|
"""Create a RuleViolation instance for this rule"""
|
|
71
88
|
return RuleViolation(
|
|
72
|
-
rule=self,
|
|
89
|
+
rule=self,
|
|
90
|
+
violation_msg=violation_msg or self.summary,
|
|
91
|
+
violation_range=violation_range,
|
|
92
|
+
fixes=fixes,
|
|
73
93
|
)
|
|
74
94
|
|
|
75
95
|
def get_definition_location(self) -> RuleLocation:
|
|
@@ -103,11 +123,16 @@ class Rule(abc.ABC, metaclass=_Rule):
|
|
|
103
123
|
|
|
104
124
|
class RuleViolation:
|
|
105
125
|
def __init__(
|
|
106
|
-
self,
|
|
126
|
+
self,
|
|
127
|
+
rule: Rule,
|
|
128
|
+
violation_msg: str,
|
|
129
|
+
violation_range: t.Optional[Range] = None,
|
|
130
|
+
fixes: t.Optional[t.List[Fix]] = None,
|
|
107
131
|
) -> None:
|
|
108
132
|
self.rule = rule
|
|
109
133
|
self.violation_msg = violation_msg
|
|
110
134
|
self.violation_range = violation_range
|
|
135
|
+
self.fixes = fixes or []
|
|
111
136
|
|
|
112
137
|
def __repr__(self) -> str:
|
|
113
138
|
return f"{self.rule.name}: {self.violation_msg}"
|
|
@@ -8,7 +8,7 @@ from sqlglot.expressions import Star
|
|
|
8
8
|
from sqlglot.helper import subclasses
|
|
9
9
|
|
|
10
10
|
from sqlmesh.core.linter.helpers import TokenPositionDetails
|
|
11
|
-
from sqlmesh.core.linter.rule import Rule, RuleViolation, Range
|
|
11
|
+
from sqlmesh.core.linter.rule import Rule, RuleViolation, Range, Fix, TextEdit
|
|
12
12
|
from sqlmesh.core.linter.definition import RuleSet
|
|
13
13
|
from sqlmesh.core.model import Model, SqlModel
|
|
14
14
|
|
|
@@ -22,7 +22,8 @@ class NoSelectStar(Rule):
|
|
|
22
22
|
return None
|
|
23
23
|
if model.query.is_star:
|
|
24
24
|
violation_range = self._get_range(model)
|
|
25
|
-
|
|
25
|
+
fixes = self._create_fixes(model, violation_range)
|
|
26
|
+
return self.violation(violation_range=violation_range, fixes=fixes)
|
|
26
27
|
return None
|
|
27
28
|
|
|
28
29
|
def _get_range(self, model: SqlModel) -> t.Optional[Range]:
|
|
@@ -37,6 +38,28 @@ class NoSelectStar(Rule):
|
|
|
37
38
|
|
|
38
39
|
return None
|
|
39
40
|
|
|
41
|
+
def _create_fixes(
|
|
42
|
+
self, model: SqlModel, violation_range: t.Optional[Range]
|
|
43
|
+
) -> t.Optional[t.List[Fix]]:
|
|
44
|
+
"""Create fixes for the SELECT * violation."""
|
|
45
|
+
if not violation_range:
|
|
46
|
+
return None
|
|
47
|
+
columns = model.columns_to_types
|
|
48
|
+
if not columns:
|
|
49
|
+
return None
|
|
50
|
+
new_text = ", ".join(columns.keys())
|
|
51
|
+
return [
|
|
52
|
+
Fix(
|
|
53
|
+
title="Replace SELECT * with explicit column list",
|
|
54
|
+
edits=[
|
|
55
|
+
TextEdit(
|
|
56
|
+
range=violation_range,
|
|
57
|
+
new_text=new_text,
|
|
58
|
+
)
|
|
59
|
+
],
|
|
60
|
+
)
|
|
61
|
+
]
|
|
62
|
+
|
|
40
63
|
|
|
41
64
|
class InvalidSelectStarExpansion(Rule):
|
|
42
65
|
def check_model(self, model: Model) -> t.Optional[RuleViolation]:
|
sqlmesh/core/model/definition.py
CHANGED
|
@@ -172,13 +172,9 @@ class EnvironmentState:
|
|
|
172
172
|
Returns:
|
|
173
173
|
The list of environment summaries to remove.
|
|
174
174
|
"""
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
env_summary
|
|
179
|
-
for env_summary in environment_summaries
|
|
180
|
-
if env_summary.expiration_ts is not None and env_summary.expiration_ts <= current_ts
|
|
181
|
-
]
|
|
175
|
+
return self._fetch_environment_summaries(
|
|
176
|
+
where=self._create_expiration_filter_expr(current_ts)
|
|
177
|
+
)
|
|
182
178
|
|
|
183
179
|
def delete_expired_environments(
|
|
184
180
|
self, current_ts: t.Optional[int] = None
|
|
@@ -225,13 +221,7 @@ class EnvironmentState:
|
|
|
225
221
|
Returns:
|
|
226
222
|
A list of all environment summaries.
|
|
227
223
|
"""
|
|
228
|
-
return
|
|
229
|
-
self._environment_summmary_from_row(row)
|
|
230
|
-
for row in fetchall(
|
|
231
|
-
self.engine_adapter,
|
|
232
|
-
self._environments_query(required_fields=list(EnvironmentSummary.all_fields())),
|
|
233
|
-
)
|
|
234
|
-
]
|
|
224
|
+
return self._fetch_environment_summaries()
|
|
235
225
|
|
|
236
226
|
def get_environment(
|
|
237
227
|
self, environment: str, lock_for_update: bool = False
|
|
@@ -327,6 +317,20 @@ class EnvironmentState:
|
|
|
327
317
|
expression=exp.Literal.number(current_ts),
|
|
328
318
|
)
|
|
329
319
|
|
|
320
|
+
def _fetch_environment_summaries(
|
|
321
|
+
self, where: t.Optional[str | exp.Expression] = None
|
|
322
|
+
) -> t.List[EnvironmentSummary]:
|
|
323
|
+
return [
|
|
324
|
+
self._environment_summmary_from_row(row)
|
|
325
|
+
for row in fetchall(
|
|
326
|
+
self.engine_adapter,
|
|
327
|
+
self._environments_query(
|
|
328
|
+
where=where,
|
|
329
|
+
required_fields=list(EnvironmentSummary.all_fields()),
|
|
330
|
+
),
|
|
331
|
+
)
|
|
332
|
+
]
|
|
333
|
+
|
|
330
334
|
|
|
331
335
|
def _environment_to_df(environment: Environment) -> pd.DataFrame:
|
|
332
336
|
import pandas as pd
|
sqlmesh/lsp/context.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from pathlib import Path
|
|
3
|
+
import uuid
|
|
3
4
|
from sqlmesh.core.context import Context
|
|
4
5
|
import typing as t
|
|
5
6
|
|
|
@@ -8,6 +9,7 @@ from sqlmesh.core.linter.definition import AnnotatedRuleViolation
|
|
|
8
9
|
from sqlmesh.lsp.custom import ModelForRendering
|
|
9
10
|
from sqlmesh.lsp.custom import AllModelsResponse, RenderModelEntry
|
|
10
11
|
from sqlmesh.lsp.uri import URI
|
|
12
|
+
from lsprotocol import types
|
|
11
13
|
|
|
12
14
|
|
|
13
15
|
@dataclass
|
|
@@ -33,8 +35,14 @@ class LSPContext:
|
|
|
33
35
|
map: t.Dict[Path, t.Union[ModelTarget, AuditTarget]]
|
|
34
36
|
_render_cache: t.Dict[Path, t.List[RenderModelEntry]]
|
|
35
37
|
_lint_cache: t.Dict[Path, t.List[AnnotatedRuleViolation]]
|
|
38
|
+
_version_id: str
|
|
39
|
+
"""
|
|
40
|
+
This is a version ID for the context. It is used to track changes to the context. It can be used to
|
|
41
|
+
return a version number to the LSP client.
|
|
42
|
+
"""
|
|
36
43
|
|
|
37
44
|
def __init__(self, context: Context) -> None:
|
|
45
|
+
self._version_id = str(uuid.uuid4())
|
|
38
46
|
self.context = context
|
|
39
47
|
self._render_cache = {}
|
|
40
48
|
self._lint_cache = {}
|
|
@@ -62,6 +70,11 @@ class LSPContext:
|
|
|
62
70
|
**audit_map,
|
|
63
71
|
}
|
|
64
72
|
|
|
73
|
+
@property
|
|
74
|
+
def version_id(self) -> str:
|
|
75
|
+
"""Get the version ID for the context."""
|
|
76
|
+
return self._version_id
|
|
77
|
+
|
|
65
78
|
def render_model(self, uri: URI) -> t.List[RenderModelEntry]:
|
|
66
79
|
"""Get rendered models for a file, using cache when available.
|
|
67
80
|
|
|
@@ -150,6 +163,86 @@ class LSPContext:
|
|
|
150
163
|
self._lint_cache[path] = diagnostics
|
|
151
164
|
return diagnostics
|
|
152
165
|
|
|
166
|
+
def get_code_actions(
|
|
167
|
+
self, uri: URI, params: types.CodeActionParams
|
|
168
|
+
) -> t.Optional[t.List[t.Union[types.Command, types.CodeAction]]]:
|
|
169
|
+
"""Get code actions for a file."""
|
|
170
|
+
|
|
171
|
+
# Get the violations (which contain the fixes)
|
|
172
|
+
violations = self.lint_model(uri)
|
|
173
|
+
|
|
174
|
+
# Convert violations to a map for quick lookup
|
|
175
|
+
# Use a hashable representation of Range as the key
|
|
176
|
+
violation_map: t.Dict[
|
|
177
|
+
t.Tuple[str, t.Tuple[int, int, int, int]], AnnotatedRuleViolation
|
|
178
|
+
] = {}
|
|
179
|
+
for violation in violations:
|
|
180
|
+
if violation.violation_range:
|
|
181
|
+
lsp_diagnostic = self.diagnostic_to_lsp_diagnostic(violation)
|
|
182
|
+
if lsp_diagnostic:
|
|
183
|
+
# Create a hashable key from the diagnostic message and range
|
|
184
|
+
key = (
|
|
185
|
+
lsp_diagnostic.message,
|
|
186
|
+
(
|
|
187
|
+
lsp_diagnostic.range.start.line,
|
|
188
|
+
lsp_diagnostic.range.start.character,
|
|
189
|
+
lsp_diagnostic.range.end.line,
|
|
190
|
+
lsp_diagnostic.range.end.character,
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
violation_map[key] = violation
|
|
194
|
+
|
|
195
|
+
# Get diagnostics in the requested range
|
|
196
|
+
diagnostics = params.context.diagnostics if params.context else []
|
|
197
|
+
|
|
198
|
+
code_actions: t.List[t.Union[types.Command, types.CodeAction]] = []
|
|
199
|
+
|
|
200
|
+
for diagnostic in diagnostics:
|
|
201
|
+
# Find the corresponding violation
|
|
202
|
+
key = (
|
|
203
|
+
diagnostic.message,
|
|
204
|
+
(
|
|
205
|
+
diagnostic.range.start.line,
|
|
206
|
+
diagnostic.range.start.character,
|
|
207
|
+
diagnostic.range.end.line,
|
|
208
|
+
diagnostic.range.end.character,
|
|
209
|
+
),
|
|
210
|
+
)
|
|
211
|
+
found_violation = violation_map.get(key)
|
|
212
|
+
|
|
213
|
+
if found_violation is not None and found_violation.fixes:
|
|
214
|
+
# Create code actions for each fix
|
|
215
|
+
for fix in found_violation.fixes:
|
|
216
|
+
# Convert our Fix to LSP TextEdits
|
|
217
|
+
text_edits = []
|
|
218
|
+
for edit in fix.edits:
|
|
219
|
+
text_edits.append(
|
|
220
|
+
types.TextEdit(
|
|
221
|
+
range=types.Range(
|
|
222
|
+
start=types.Position(
|
|
223
|
+
line=edit.range.start.line,
|
|
224
|
+
character=edit.range.start.character,
|
|
225
|
+
),
|
|
226
|
+
end=types.Position(
|
|
227
|
+
line=edit.range.end.line,
|
|
228
|
+
character=edit.range.end.character,
|
|
229
|
+
),
|
|
230
|
+
),
|
|
231
|
+
new_text=edit.new_text,
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# Create the code action
|
|
236
|
+
code_action = types.CodeAction(
|
|
237
|
+
title=fix.title,
|
|
238
|
+
kind=types.CodeActionKind.QuickFix,
|
|
239
|
+
diagnostics=[diagnostic],
|
|
240
|
+
edit=types.WorkspaceEdit(changes={params.text_document.uri: text_edits}),
|
|
241
|
+
)
|
|
242
|
+
code_actions.append(code_action)
|
|
243
|
+
|
|
244
|
+
return code_actions if code_actions else None
|
|
245
|
+
|
|
153
246
|
def list_of_models_for_rendering(self) -> t.List[ModelForRendering]:
|
|
154
247
|
"""Get a list of models for rendering.
|
|
155
248
|
|
|
@@ -186,3 +279,68 @@ class LSPContext:
|
|
|
186
279
|
from sqlmesh.lsp.completions import get_sql_completions
|
|
187
280
|
|
|
188
281
|
return get_sql_completions(self, uri, file_content)
|
|
282
|
+
|
|
283
|
+
@staticmethod
|
|
284
|
+
def diagnostics_to_lsp_diagnostics(
|
|
285
|
+
diagnostics: t.List[AnnotatedRuleViolation],
|
|
286
|
+
) -> t.List[types.Diagnostic]:
|
|
287
|
+
"""
|
|
288
|
+
Converts a list of AnnotatedRuleViolations to a list of LSP diagnostics. It will remove duplicates based on the message and range.
|
|
289
|
+
"""
|
|
290
|
+
lsp_diagnostics = {}
|
|
291
|
+
for diagnostic in diagnostics:
|
|
292
|
+
lsp_diagnostic = LSPContext.diagnostic_to_lsp_diagnostic(diagnostic)
|
|
293
|
+
if lsp_diagnostic is not None:
|
|
294
|
+
# Create a unique key combining message and range
|
|
295
|
+
diagnostic_key = (
|
|
296
|
+
lsp_diagnostic.message,
|
|
297
|
+
lsp_diagnostic.range.start.line,
|
|
298
|
+
lsp_diagnostic.range.start.character,
|
|
299
|
+
lsp_diagnostic.range.end.line,
|
|
300
|
+
lsp_diagnostic.range.end.character,
|
|
301
|
+
)
|
|
302
|
+
if diagnostic_key not in lsp_diagnostics:
|
|
303
|
+
lsp_diagnostics[diagnostic_key] = lsp_diagnostic
|
|
304
|
+
return list(lsp_diagnostics.values())
|
|
305
|
+
|
|
306
|
+
@staticmethod
|
|
307
|
+
def diagnostic_to_lsp_diagnostic(
|
|
308
|
+
diagnostic: AnnotatedRuleViolation,
|
|
309
|
+
) -> t.Optional[types.Diagnostic]:
|
|
310
|
+
if diagnostic.model._path is None:
|
|
311
|
+
return None
|
|
312
|
+
if not diagnostic.violation_range:
|
|
313
|
+
with open(diagnostic.model._path, "r", encoding="utf-8") as file:
|
|
314
|
+
lines = file.readlines()
|
|
315
|
+
diagnostic_range = types.Range(
|
|
316
|
+
start=types.Position(line=0, character=0),
|
|
317
|
+
end=types.Position(line=len(lines) - 1, character=len(lines[-1])),
|
|
318
|
+
)
|
|
319
|
+
else:
|
|
320
|
+
diagnostic_range = types.Range(
|
|
321
|
+
start=types.Position(
|
|
322
|
+
line=diagnostic.violation_range.start.line,
|
|
323
|
+
character=diagnostic.violation_range.start.character,
|
|
324
|
+
),
|
|
325
|
+
end=types.Position(
|
|
326
|
+
line=diagnostic.violation_range.end.line,
|
|
327
|
+
character=diagnostic.violation_range.end.character,
|
|
328
|
+
),
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
# Get rule definition location for diagnostics link
|
|
332
|
+
rule_location = diagnostic.rule.get_definition_location()
|
|
333
|
+
rule_uri_wihout_extension = URI.from_path(rule_location.file_path)
|
|
334
|
+
rule_uri = f"{rule_uri_wihout_extension.value}#L{rule_location.start_line}"
|
|
335
|
+
|
|
336
|
+
# Use URI format to create a link for "related information"
|
|
337
|
+
return types.Diagnostic(
|
|
338
|
+
range=diagnostic_range,
|
|
339
|
+
message=diagnostic.violation_msg,
|
|
340
|
+
severity=types.DiagnosticSeverity.Error
|
|
341
|
+
if diagnostic.violation_type == "error"
|
|
342
|
+
else types.DiagnosticSeverity.Warning,
|
|
343
|
+
source="sqlmesh",
|
|
344
|
+
code=diagnostic.rule.name,
|
|
345
|
+
code_description=types.CodeDescription(href=rule_uri),
|
|
346
|
+
)
|