sqlmesh 0.189.2.dev5__py3-none-any.whl → 0.189.3.dev1__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/core/linter/definition.py +5 -2
- sqlmesh/core/linter/helpers.py +115 -0
- sqlmesh/core/linter/rule.py +29 -3
- sqlmesh/core/linter/rules/builtin.py +20 -3
- sqlmesh/lsp/context.py +1 -1
- sqlmesh/lsp/hints.py +136 -0
- sqlmesh/lsp/main.py +39 -6
- sqlmesh/lsp/reference.py +49 -83
- {sqlmesh-0.189.2.dev5.dist-info → sqlmesh-0.189.3.dev1.dist-info}/METADATA +1 -1
- {sqlmesh-0.189.2.dev5.dist-info → sqlmesh-0.189.3.dev1.dist-info}/RECORD +15 -13
- {sqlmesh-0.189.2.dev5.dist-info → sqlmesh-0.189.3.dev1.dist-info}/WHEEL +0 -0
- {sqlmesh-0.189.2.dev5.dist-info → sqlmesh-0.189.3.dev1.dist-info}/entry_points.txt +0 -0
- {sqlmesh-0.189.2.dev5.dist-info → sqlmesh-0.189.3.dev1.dist-info}/licenses/LICENSE +0 -0
- {sqlmesh-0.189.2.dev5.dist-info → sqlmesh-0.189.3.dev1.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.189.
|
|
21
|
-
__version_tuple__ = version_tuple = (0, 189,
|
|
20
|
+
__version__ = version = '0.189.3.dev1'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 189, 3, 'dev1')
|
|
@@ -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
|
|
11
|
+
from sqlmesh.core.linter.rule import Rule, RuleViolation, Range
|
|
12
12
|
from sqlmesh.core.console import LinterConsole, get_console
|
|
13
13
|
|
|
14
14
|
if t.TYPE_CHECKING:
|
|
@@ -74,6 +74,7 @@ class Linter:
|
|
|
74
74
|
violation_msg=violation.violation_msg,
|
|
75
75
|
model=model,
|
|
76
76
|
violation_type="error",
|
|
77
|
+
violation_range=violation.violation_range,
|
|
77
78
|
)
|
|
78
79
|
for violation in error_violations
|
|
79
80
|
] + [
|
|
@@ -82,6 +83,7 @@ class Linter:
|
|
|
82
83
|
violation_msg=violation.violation_msg,
|
|
83
84
|
model=model,
|
|
84
85
|
violation_type="warning",
|
|
86
|
+
violation_range=violation.violation_range,
|
|
85
87
|
)
|
|
86
88
|
for violation in warn_violations
|
|
87
89
|
]
|
|
@@ -149,7 +151,8 @@ class AnnotatedRuleViolation(RuleViolation):
|
|
|
149
151
|
violation_msg: str,
|
|
150
152
|
model: Model,
|
|
151
153
|
violation_type: t.Literal["error", "warning"],
|
|
154
|
+
violation_range: t.Optional[Range] = None,
|
|
152
155
|
) -> None:
|
|
153
|
-
super().__init__(rule, violation_msg)
|
|
156
|
+
super().__init__(rule, violation_msg, violation_range)
|
|
154
157
|
self.model = model
|
|
155
158
|
self.violation_type = violation_type
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from sqlmesh.core.linter.rule import Position, Range
|
|
4
|
+
from sqlmesh.utils.pydantic import PydanticModel
|
|
5
|
+
import typing as t
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TokenPositionDetails(PydanticModel):
|
|
9
|
+
"""
|
|
10
|
+
Details about a token's position in the source code in the structure provided by SQLGlot.
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
line (int): The line that the token ends on.
|
|
14
|
+
col (int): The column that the token ends on.
|
|
15
|
+
start (int): The start index of the token.
|
|
16
|
+
end (int): The ending index of the token.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
line: int
|
|
20
|
+
col: int
|
|
21
|
+
start: int
|
|
22
|
+
end: int
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def from_meta(meta: t.Dict[str, int]) -> "TokenPositionDetails":
|
|
26
|
+
return TokenPositionDetails(
|
|
27
|
+
line=meta["line"],
|
|
28
|
+
col=meta["col"],
|
|
29
|
+
start=meta["start"],
|
|
30
|
+
end=meta["end"],
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
def to_range(self, read_file: t.Optional[t.List[str]]) -> Range:
|
|
34
|
+
"""
|
|
35
|
+
Convert a TokenPositionDetails object to a Range object.
|
|
36
|
+
|
|
37
|
+
In the circumstances where the token's start and end positions are the same,
|
|
38
|
+
there is no need for a read_file parameter, as the range can be derived from the token's
|
|
39
|
+
line and column. This is an optimization to avoid unnecessary file reads and should
|
|
40
|
+
only be used when the token represents a single character or position in the file.
|
|
41
|
+
|
|
42
|
+
If the token's start and end positions are different, the read_file parameter is required.
|
|
43
|
+
|
|
44
|
+
:param read_file: List of lines from the file. Optional
|
|
45
|
+
:return: A Range object representing the token's position
|
|
46
|
+
"""
|
|
47
|
+
if self.start == self.end:
|
|
48
|
+
# If the start and end positions are the same, we can create a range directly
|
|
49
|
+
return Range(
|
|
50
|
+
start=Position(line=self.line - 1, character=self.col - 1),
|
|
51
|
+
end=Position(line=self.line - 1, character=self.col),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
if read_file is None:
|
|
55
|
+
raise ValueError("read_file must be provided when start and end positions differ.")
|
|
56
|
+
|
|
57
|
+
# Convert from 1-indexed to 0-indexed for line only
|
|
58
|
+
end_line_0 = self.line - 1
|
|
59
|
+
end_col_0 = self.col
|
|
60
|
+
|
|
61
|
+
# Find the start line and column by counting backwards from the end position
|
|
62
|
+
start_pos = self.start
|
|
63
|
+
end_pos = self.end
|
|
64
|
+
|
|
65
|
+
# Initialize with the end position
|
|
66
|
+
start_line_0 = end_line_0
|
|
67
|
+
start_col_0 = end_col_0 - (end_pos - start_pos + 1)
|
|
68
|
+
|
|
69
|
+
# If start_col_0 is negative, we need to go back to previous lines
|
|
70
|
+
while start_col_0 < 0 and start_line_0 > 0:
|
|
71
|
+
start_line_0 -= 1
|
|
72
|
+
start_col_0 += len(read_file[start_line_0])
|
|
73
|
+
# Account for newline character
|
|
74
|
+
if start_col_0 >= 0:
|
|
75
|
+
break
|
|
76
|
+
start_col_0 += 1 # For the newline character
|
|
77
|
+
|
|
78
|
+
# Ensure we don't have negative values
|
|
79
|
+
start_col_0 = max(0, start_col_0)
|
|
80
|
+
return Range(
|
|
81
|
+
start=Position(line=start_line_0, character=start_col_0),
|
|
82
|
+
end=Position(line=end_line_0, character=end_col_0),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def read_range_from_file(file: Path, text_range: Range) -> str:
|
|
87
|
+
"""
|
|
88
|
+
Read the file and return the content within the specified range.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
file: Path to the file to read
|
|
92
|
+
text_range: The range of text to extract
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
The content within the specified range
|
|
96
|
+
"""
|
|
97
|
+
with file.open("r", encoding="utf-8") as f:
|
|
98
|
+
lines = f.readlines()
|
|
99
|
+
|
|
100
|
+
# Ensure the range is within bounds
|
|
101
|
+
start_line = max(0, text_range.start.line)
|
|
102
|
+
end_line = min(len(lines), text_range.end.line + 1)
|
|
103
|
+
|
|
104
|
+
if start_line >= end_line:
|
|
105
|
+
return ""
|
|
106
|
+
|
|
107
|
+
# Extract the relevant portions of each line
|
|
108
|
+
result = []
|
|
109
|
+
for i in range(start_line, end_line):
|
|
110
|
+
line = lines[i]
|
|
111
|
+
start_char = text_range.start.character if i == text_range.start.line else 0
|
|
112
|
+
end_char = text_range.end.character if i == text_range.end.line else len(line)
|
|
113
|
+
result.append(line[start_char:end_char])
|
|
114
|
+
|
|
115
|
+
return "".join(result)
|
sqlmesh/core/linter/rule.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import abc
|
|
4
|
+
from dataclasses import dataclass
|
|
4
5
|
|
|
5
6
|
from sqlmesh.core.model import Model
|
|
6
7
|
|
|
@@ -22,6 +23,22 @@ class RuleLocation(PydanticModel):
|
|
|
22
23
|
start_line: t.Optional[int] = None
|
|
23
24
|
|
|
24
25
|
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class Position:
|
|
28
|
+
"""The position of a rule violation in a file, the position follows the LSP standard."""
|
|
29
|
+
|
|
30
|
+
line: int
|
|
31
|
+
character: int
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class Range:
|
|
36
|
+
"""The range of a rule violation in a file. The range follows the LSP standard."""
|
|
37
|
+
|
|
38
|
+
start: Position
|
|
39
|
+
end: Position
|
|
40
|
+
|
|
41
|
+
|
|
25
42
|
class _Rule(abc.ABCMeta):
|
|
26
43
|
def __new__(cls: Type[_Rule], clsname: str, bases: t.Tuple, attrs: t.Dict) -> _Rule:
|
|
27
44
|
attrs["name"] = clsname.lower()
|
|
@@ -45,9 +62,15 @@ class Rule(abc.ABC, metaclass=_Rule):
|
|
|
45
62
|
"""A summary of what this rule checks for."""
|
|
46
63
|
return self.__doc__ or ""
|
|
47
64
|
|
|
48
|
-
def violation(
|
|
65
|
+
def violation(
|
|
66
|
+
self,
|
|
67
|
+
violation_msg: t.Optional[str] = None,
|
|
68
|
+
violation_range: t.Optional[Range] = None,
|
|
69
|
+
) -> RuleViolation:
|
|
49
70
|
"""Create a RuleViolation instance for this rule"""
|
|
50
|
-
return RuleViolation(
|
|
71
|
+
return RuleViolation(
|
|
72
|
+
rule=self, violation_msg=violation_msg or self.summary, violation_range=violation_range
|
|
73
|
+
)
|
|
51
74
|
|
|
52
75
|
def get_definition_location(self) -> RuleLocation:
|
|
53
76
|
"""Return the file path and position information for this rule.
|
|
@@ -79,9 +102,12 @@ class Rule(abc.ABC, metaclass=_Rule):
|
|
|
79
102
|
|
|
80
103
|
|
|
81
104
|
class RuleViolation:
|
|
82
|
-
def __init__(
|
|
105
|
+
def __init__(
|
|
106
|
+
self, rule: Rule, violation_msg: str, violation_range: t.Optional[Range] = None
|
|
107
|
+
) -> None:
|
|
83
108
|
self.rule = rule
|
|
84
109
|
self.violation_msg = violation_msg
|
|
110
|
+
self.violation_range = violation_range
|
|
85
111
|
|
|
86
112
|
def __repr__(self) -> str:
|
|
87
113
|
return f"{self.rule.name}: {self.violation_msg}"
|
|
@@ -4,9 +4,11 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import typing as t
|
|
6
6
|
|
|
7
|
+
from sqlglot.expressions import Star
|
|
7
8
|
from sqlglot.helper import subclasses
|
|
8
9
|
|
|
9
|
-
from sqlmesh.core.linter.
|
|
10
|
+
from sqlmesh.core.linter.helpers import TokenPositionDetails
|
|
11
|
+
from sqlmesh.core.linter.rule import Rule, RuleViolation, Range
|
|
10
12
|
from sqlmesh.core.linter.definition import RuleSet
|
|
11
13
|
from sqlmesh.core.model import Model, SqlModel
|
|
12
14
|
|
|
@@ -15,10 +17,25 @@ class NoSelectStar(Rule):
|
|
|
15
17
|
"""Query should not contain SELECT * on its outer most projections, even if it can be expanded."""
|
|
16
18
|
|
|
17
19
|
def check_model(self, model: Model) -> t.Optional[RuleViolation]:
|
|
20
|
+
# Only applies to SQL models, as other model types do not have a query.
|
|
18
21
|
if not isinstance(model, SqlModel):
|
|
19
22
|
return None
|
|
20
|
-
|
|
21
|
-
|
|
23
|
+
if model.query.is_star:
|
|
24
|
+
violation_range = self._get_range(model)
|
|
25
|
+
return self.violation(violation_range=violation_range)
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
def _get_range(self, model: SqlModel) -> t.Optional[Range]:
|
|
29
|
+
"""Get the range of the violation if available."""
|
|
30
|
+
try:
|
|
31
|
+
if len(model.query.expressions) == 1 and isinstance(model.query.expressions[0], Star):
|
|
32
|
+
return TokenPositionDetails.from_meta(model.query.expressions[0].meta).to_range(
|
|
33
|
+
None
|
|
34
|
+
)
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
return None
|
|
22
39
|
|
|
23
40
|
|
|
24
41
|
class InvalidSelectStarExpansion(Rule):
|
sqlmesh/lsp/context.py
CHANGED
|
@@ -5,7 +5,7 @@ import typing as t
|
|
|
5
5
|
|
|
6
6
|
from sqlmesh.core.model.definition import SqlModel
|
|
7
7
|
from sqlmesh.core.linter.definition import AnnotatedRuleViolation
|
|
8
|
-
from sqlmesh.lsp.custom import
|
|
8
|
+
from sqlmesh.lsp.custom import ModelForRendering
|
|
9
9
|
from sqlmesh.lsp.custom import AllModelsResponse, RenderModelEntry
|
|
10
10
|
from sqlmesh.lsp.uri import URI
|
|
11
11
|
|
sqlmesh/lsp/hints.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Type hinting on SQLMesh models"""
|
|
2
|
+
|
|
3
|
+
import typing as t
|
|
4
|
+
|
|
5
|
+
from lsprotocol import types
|
|
6
|
+
|
|
7
|
+
from sqlglot import exp
|
|
8
|
+
from sqlglot.expressions import Expression
|
|
9
|
+
from sqlglot.optimizer.normalize_identifiers import normalize_identifiers
|
|
10
|
+
from sqlmesh.core.model.definition import SqlModel
|
|
11
|
+
from sqlmesh.lsp.context import LSPContext, ModelTarget
|
|
12
|
+
from sqlmesh.lsp.uri import URI
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_hints(
|
|
16
|
+
lsp_context: LSPContext,
|
|
17
|
+
document_uri: URI,
|
|
18
|
+
start_line: int,
|
|
19
|
+
end_line: int,
|
|
20
|
+
) -> t.List[types.InlayHint]:
|
|
21
|
+
"""
|
|
22
|
+
Get type hints for certain lines in a document
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
lint_context: The LSP context
|
|
26
|
+
document_uri: The URI of the document
|
|
27
|
+
start_line: the starting line to get hints for
|
|
28
|
+
end_line: the ending line to get hints for
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
A list of hints to apply to the document
|
|
32
|
+
"""
|
|
33
|
+
path = document_uri.to_path()
|
|
34
|
+
if path.suffix != ".sql":
|
|
35
|
+
return []
|
|
36
|
+
|
|
37
|
+
if path not in lsp_context.map:
|
|
38
|
+
return []
|
|
39
|
+
|
|
40
|
+
file_info = lsp_context.map[path]
|
|
41
|
+
|
|
42
|
+
# Process based on whether it's a model or standalone audit
|
|
43
|
+
if not isinstance(file_info, ModelTarget):
|
|
44
|
+
return []
|
|
45
|
+
|
|
46
|
+
# It's a model
|
|
47
|
+
model = lsp_context.context.get_model(
|
|
48
|
+
model_or_snapshot=file_info.names[0], raise_if_missing=False
|
|
49
|
+
)
|
|
50
|
+
if not isinstance(model, SqlModel):
|
|
51
|
+
return []
|
|
52
|
+
|
|
53
|
+
query = model.query
|
|
54
|
+
dialect = model.dialect
|
|
55
|
+
columns_to_types = model.columns_to_types or {}
|
|
56
|
+
|
|
57
|
+
return _get_type_hints_for_model_from_query(
|
|
58
|
+
query, dialect, columns_to_types, start_line, end_line
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _get_type_hints_for_select(
|
|
63
|
+
expression: exp.Expression,
|
|
64
|
+
dialect: str,
|
|
65
|
+
columns_to_types: t.Dict[str, exp.DataType],
|
|
66
|
+
start_line: int,
|
|
67
|
+
end_line: int,
|
|
68
|
+
) -> t.List[types.InlayHint]:
|
|
69
|
+
hints: t.List[types.InlayHint] = []
|
|
70
|
+
|
|
71
|
+
for select_exp in expression.expressions:
|
|
72
|
+
if isinstance(select_exp, exp.Alias):
|
|
73
|
+
if isinstance(select_exp.this, exp.Cast):
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
meta = select_exp.args["alias"].meta
|
|
77
|
+
|
|
78
|
+
elif isinstance(select_exp, exp.Column):
|
|
79
|
+
meta = select_exp.parts[-1].meta
|
|
80
|
+
else:
|
|
81
|
+
continue
|
|
82
|
+
|
|
83
|
+
if "line" not in meta or "col" not in meta:
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
line = meta["line"]
|
|
87
|
+
col = meta["col"]
|
|
88
|
+
|
|
89
|
+
# Lines from sqlglot are 1 based
|
|
90
|
+
line -= 1
|
|
91
|
+
|
|
92
|
+
if line < start_line or line > end_line:
|
|
93
|
+
continue
|
|
94
|
+
|
|
95
|
+
name = select_exp.alias_or_name
|
|
96
|
+
data_type = columns_to_types.get(name)
|
|
97
|
+
|
|
98
|
+
if not data_type or data_type.is_type(exp.DataType.Type.UNKNOWN):
|
|
99
|
+
continue
|
|
100
|
+
|
|
101
|
+
type_label = data_type.sql(dialect)
|
|
102
|
+
hints.append(
|
|
103
|
+
types.InlayHint(
|
|
104
|
+
label=f"::{type_label}",
|
|
105
|
+
kind=types.InlayHintKind.Type,
|
|
106
|
+
padding_left=False,
|
|
107
|
+
padding_right=True,
|
|
108
|
+
position=types.Position(line=line, character=col),
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return hints
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _get_type_hints_for_model_from_query(
|
|
116
|
+
query: Expression,
|
|
117
|
+
dialect: str,
|
|
118
|
+
columns_to_types: t.Dict[str, exp.DataType],
|
|
119
|
+
start_line: int,
|
|
120
|
+
end_line: int,
|
|
121
|
+
) -> t.List[types.InlayHint]:
|
|
122
|
+
hints: t.List[types.InlayHint] = []
|
|
123
|
+
try:
|
|
124
|
+
query = normalize_identifiers(query.copy(), dialect=dialect)
|
|
125
|
+
|
|
126
|
+
# Return the hints for top level selects (model definition columns only)
|
|
127
|
+
return [
|
|
128
|
+
hint
|
|
129
|
+
for q in query.walk(prune=lambda n: not isinstance(n, exp.SetOperation))
|
|
130
|
+
if isinstance(select := q.unnest(), exp.Select)
|
|
131
|
+
for hint in _get_type_hints_for_select(
|
|
132
|
+
q, dialect, columns_to_types, start_line, end_line
|
|
133
|
+
)
|
|
134
|
+
]
|
|
135
|
+
except Exception:
|
|
136
|
+
return []
|
sqlmesh/lsp/main.py
CHANGED
|
@@ -36,6 +36,7 @@ from sqlmesh.lsp.custom import (
|
|
|
36
36
|
RenderModelRequest,
|
|
37
37
|
RenderModelResponse,
|
|
38
38
|
)
|
|
39
|
+
from sqlmesh.lsp.hints import get_hints
|
|
39
40
|
from sqlmesh.lsp.reference import get_references, get_cte_references
|
|
40
41
|
from sqlmesh.lsp.uri import URI
|
|
41
42
|
from web.server.api.endpoints.lineage import column_lineage, model_lineage
|
|
@@ -334,6 +335,25 @@ class SQLMeshLanguageServer:
|
|
|
334
335
|
)
|
|
335
336
|
return None
|
|
336
337
|
|
|
338
|
+
@self.server.feature(types.TEXT_DOCUMENT_INLAY_HINT)
|
|
339
|
+
def inlay_hint(
|
|
340
|
+
ls: LanguageServer, params: types.InlayHintParams
|
|
341
|
+
) -> t.List[types.InlayHint]:
|
|
342
|
+
"""Implement type hints for sql columns as inlay hints"""
|
|
343
|
+
try:
|
|
344
|
+
uri = URI(params.text_document.uri)
|
|
345
|
+
self._ensure_context_for_document(uri)
|
|
346
|
+
if self.lsp_context is None:
|
|
347
|
+
raise RuntimeError(f"No context found for document: {uri}")
|
|
348
|
+
|
|
349
|
+
start_line = params.range.start.line
|
|
350
|
+
end_line = params.range.end.line
|
|
351
|
+
hints = get_hints(self.lsp_context, uri, start_line, end_line)
|
|
352
|
+
return hints
|
|
353
|
+
|
|
354
|
+
except Exception as e:
|
|
355
|
+
return []
|
|
356
|
+
|
|
337
357
|
@self.server.feature(types.TEXT_DOCUMENT_DEFINITION)
|
|
338
358
|
def goto_definition(
|
|
339
359
|
ls: LanguageServer, params: types.DefinitionParams
|
|
@@ -655,8 +675,24 @@ class SQLMeshLanguageServer:
|
|
|
655
675
|
) -> t.Optional[types.Diagnostic]:
|
|
656
676
|
if diagnostic.model._path is None:
|
|
657
677
|
return None
|
|
658
|
-
|
|
659
|
-
|
|
678
|
+
if not diagnostic.violation_range:
|
|
679
|
+
with open(diagnostic.model._path, "r", encoding="utf-8") as file:
|
|
680
|
+
lines = file.readlines()
|
|
681
|
+
range = types.Range(
|
|
682
|
+
start=types.Position(line=0, character=0),
|
|
683
|
+
end=types.Position(line=len(lines) - 1, character=len(lines[-1])),
|
|
684
|
+
)
|
|
685
|
+
else:
|
|
686
|
+
range = types.Range(
|
|
687
|
+
start=types.Position(
|
|
688
|
+
line=diagnostic.violation_range.start.line,
|
|
689
|
+
character=diagnostic.violation_range.start.character,
|
|
690
|
+
),
|
|
691
|
+
end=types.Position(
|
|
692
|
+
line=diagnostic.violation_range.end.line,
|
|
693
|
+
character=diagnostic.violation_range.end.character,
|
|
694
|
+
),
|
|
695
|
+
)
|
|
660
696
|
|
|
661
697
|
# Get rule definition location for diagnostics link
|
|
662
698
|
rule_location = diagnostic.rule.get_definition_location()
|
|
@@ -665,10 +701,7 @@ class SQLMeshLanguageServer:
|
|
|
665
701
|
|
|
666
702
|
# Use URI format to create a link for "related information"
|
|
667
703
|
return types.Diagnostic(
|
|
668
|
-
range=
|
|
669
|
-
start=types.Position(line=0, character=0),
|
|
670
|
-
end=types.Position(line=len(lines), character=len(lines[-1])),
|
|
671
|
-
),
|
|
704
|
+
range=range,
|
|
672
705
|
message=diagnostic.violation_msg,
|
|
673
706
|
severity=types.DiagnosticSeverity.Error
|
|
674
707
|
if diagnostic.violation_type == "error"
|
sqlmesh/lsp/reference.py
CHANGED
|
@@ -4,6 +4,11 @@ from pathlib import Path
|
|
|
4
4
|
|
|
5
5
|
from sqlmesh.core.audit import StandaloneAudit
|
|
6
6
|
from sqlmesh.core.dialect import normalize_model_name
|
|
7
|
+
from sqlmesh.core.linter.helpers import (
|
|
8
|
+
TokenPositionDetails,
|
|
9
|
+
Range as SQLMeshRange,
|
|
10
|
+
Position as SQLMeshPosition,
|
|
11
|
+
)
|
|
7
12
|
from sqlmesh.core.model.definition import SqlModel
|
|
8
13
|
from sqlmesh.lsp.context import LSPContext, ModelTarget, AuditTarget
|
|
9
14
|
from sqlglot import exp
|
|
@@ -156,12 +161,17 @@ def get_model_definitions_for_a_path(
|
|
|
156
161
|
if isinstance(alias, exp.TableAlias):
|
|
157
162
|
identifier = alias.this
|
|
158
163
|
if isinstance(identifier, exp.Identifier):
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
)
|
|
164
|
+
target_range_sqlmesh = TokenPositionDetails.from_meta(
|
|
165
|
+
identifier.meta
|
|
166
|
+
).to_range(read_file)
|
|
167
|
+
table_range_sqlmesh = TokenPositionDetails.from_meta(
|
|
168
|
+
table.this.meta
|
|
169
|
+
).to_range(read_file)
|
|
170
|
+
|
|
171
|
+
# Convert SQLMesh Range to LSP Range
|
|
172
|
+
target_range = to_lsp_range(target_range_sqlmesh)
|
|
173
|
+
table_range = to_lsp_range(table_range_sqlmesh)
|
|
174
|
+
|
|
165
175
|
references.append(
|
|
166
176
|
Reference(
|
|
167
177
|
uri=document_uri.value, # Same file
|
|
@@ -203,25 +213,26 @@ def get_model_definitions_for_a_path(
|
|
|
203
213
|
|
|
204
214
|
# Extract metadata for positioning
|
|
205
215
|
table_meta = TokenPositionDetails.from_meta(table.this.meta)
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
216
|
+
table_range_sqlmesh = table_meta.to_range(read_file)
|
|
217
|
+
start_pos_sqlmesh = table_range_sqlmesh.start
|
|
218
|
+
end_pos_sqlmesh = table_range_sqlmesh.end
|
|
209
219
|
|
|
210
220
|
# If there's a catalog or database qualifier, adjust the start position
|
|
211
221
|
catalog_or_db = table.args.get("catalog") or table.args.get("db")
|
|
212
222
|
if catalog_or_db is not None:
|
|
213
223
|
catalog_or_db_meta = TokenPositionDetails.from_meta(catalog_or_db.meta)
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
)
|
|
217
|
-
start_pos = catalog_or_db_range.start
|
|
224
|
+
catalog_or_db_range_sqlmesh = catalog_or_db_meta.to_range(read_file)
|
|
225
|
+
start_pos_sqlmesh = catalog_or_db_range_sqlmesh.start
|
|
218
226
|
|
|
219
227
|
description = generate_markdown_description(referenced_model)
|
|
220
228
|
|
|
221
229
|
references.append(
|
|
222
230
|
Reference(
|
|
223
231
|
uri=referenced_model_uri.value,
|
|
224
|
-
range=Range(
|
|
232
|
+
range=Range(
|
|
233
|
+
start=to_lsp_position(start_pos_sqlmesh),
|
|
234
|
+
end=to_lsp_position(end_pos_sqlmesh),
|
|
235
|
+
),
|
|
225
236
|
markdown_description=description,
|
|
226
237
|
)
|
|
227
238
|
)
|
|
@@ -229,71 +240,6 @@ def get_model_definitions_for_a_path(
|
|
|
229
240
|
return references
|
|
230
241
|
|
|
231
242
|
|
|
232
|
-
class TokenPositionDetails(PydanticModel):
|
|
233
|
-
"""
|
|
234
|
-
Details about a token's position in the source code.
|
|
235
|
-
|
|
236
|
-
Attributes:
|
|
237
|
-
line (int): The line that the token ends on.
|
|
238
|
-
col (int): The column that the token ends on.
|
|
239
|
-
start (int): The start index of the token.
|
|
240
|
-
end (int): The ending index of the token.
|
|
241
|
-
"""
|
|
242
|
-
|
|
243
|
-
line: int
|
|
244
|
-
col: int
|
|
245
|
-
start: int
|
|
246
|
-
end: int
|
|
247
|
-
|
|
248
|
-
@staticmethod
|
|
249
|
-
def from_meta(meta: t.Dict[str, int]) -> "TokenPositionDetails":
|
|
250
|
-
return TokenPositionDetails(
|
|
251
|
-
line=meta["line"],
|
|
252
|
-
col=meta["col"],
|
|
253
|
-
start=meta["start"],
|
|
254
|
-
end=meta["end"],
|
|
255
|
-
)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
def _range_from_token_position_details(
|
|
259
|
-
token_position_details: TokenPositionDetails, read_file: t.List[str]
|
|
260
|
-
) -> Range:
|
|
261
|
-
"""
|
|
262
|
-
Convert a TokenPositionDetails object to a Range object.
|
|
263
|
-
|
|
264
|
-
:param token_position_details: Details about a token's position
|
|
265
|
-
:param read_file: List of lines from the file
|
|
266
|
-
:return: A Range object representing the token's position
|
|
267
|
-
"""
|
|
268
|
-
# Convert from 1-indexed to 0-indexed for line only
|
|
269
|
-
end_line_0 = token_position_details.line - 1
|
|
270
|
-
end_col_0 = token_position_details.col
|
|
271
|
-
|
|
272
|
-
# Find the start line and column by counting backwards from the end position
|
|
273
|
-
start_pos = token_position_details.start
|
|
274
|
-
end_pos = token_position_details.end
|
|
275
|
-
|
|
276
|
-
# Initialize with the end position
|
|
277
|
-
start_line_0 = end_line_0
|
|
278
|
-
start_col_0 = end_col_0 - (end_pos - start_pos + 1)
|
|
279
|
-
|
|
280
|
-
# If start_col_0 is negative, we need to go back to previous lines
|
|
281
|
-
while start_col_0 < 0 and start_line_0 > 0:
|
|
282
|
-
start_line_0 -= 1
|
|
283
|
-
start_col_0 += len(read_file[start_line_0])
|
|
284
|
-
# Account for newline character
|
|
285
|
-
if start_col_0 >= 0:
|
|
286
|
-
break
|
|
287
|
-
start_col_0 += 1 # For the newline character
|
|
288
|
-
|
|
289
|
-
# Ensure we don't have negative values
|
|
290
|
-
start_col_0 = max(0, start_col_0)
|
|
291
|
-
return Range(
|
|
292
|
-
start=Position(line=start_line_0, character=start_col_0),
|
|
293
|
-
end=Position(line=end_line_0, character=end_col_0),
|
|
294
|
-
)
|
|
295
|
-
|
|
296
|
-
|
|
297
243
|
def get_macro_definitions_for_a_path(
|
|
298
244
|
lsp_context: LSPContext, document_uri: URI
|
|
299
245
|
) -> t.List[Reference]:
|
|
@@ -373,11 +319,10 @@ def get_macro_reference(
|
|
|
373
319
|
try:
|
|
374
320
|
# Get the position of the macro invocation in the source file first
|
|
375
321
|
if hasattr(node, "meta") and node.meta:
|
|
376
|
-
|
|
377
|
-
macro_range = _range_from_token_position_details(token_details, read_file)
|
|
322
|
+
macro_range = TokenPositionDetails.from_meta(node.meta).to_range(read_file)
|
|
378
323
|
|
|
379
324
|
# Check if it's a built-in method
|
|
380
|
-
if builtin := get_built_in_macro_reference(macro_name, macro_range):
|
|
325
|
+
if builtin := get_built_in_macro_reference(macro_name, to_lsp_range(macro_range)):
|
|
381
326
|
return builtin
|
|
382
327
|
else:
|
|
383
328
|
# Skip if we can't get the position
|
|
@@ -429,7 +374,7 @@ def get_macro_reference(
|
|
|
429
374
|
|
|
430
375
|
return Reference(
|
|
431
376
|
uri=macro_uri.value,
|
|
432
|
-
range=macro_range,
|
|
377
|
+
range=to_lsp_range(macro_range),
|
|
433
378
|
target_range=Range(
|
|
434
379
|
start=Position(line=start_line - 1, character=0),
|
|
435
380
|
end=Position(line=end_line - 1, character=get_length_of_end_line),
|
|
@@ -544,3 +489,24 @@ def _position_within_range(position: Position, range: Range) -> bool:
|
|
|
544
489
|
range.end.line > position.line
|
|
545
490
|
or (range.end.line == position.line and range.end.character >= position.character)
|
|
546
491
|
)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def to_lsp_range(
|
|
495
|
+
range: SQLMeshRange,
|
|
496
|
+
) -> Range:
|
|
497
|
+
"""
|
|
498
|
+
Converts a SQLMesh Range to an LSP Range.
|
|
499
|
+
"""
|
|
500
|
+
return Range(
|
|
501
|
+
start=Position(line=range.start.line, character=range.start.character),
|
|
502
|
+
end=Position(line=range.end.line, character=range.end.character),
|
|
503
|
+
)
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def to_lsp_position(
|
|
507
|
+
position: SQLMeshPosition,
|
|
508
|
+
) -> Position:
|
|
509
|
+
"""
|
|
510
|
+
Converts a SQLMesh Position to an LSP Position.
|
|
511
|
+
"""
|
|
512
|
+
return Position(line=position.line, character=position.character)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
sqlmesh/__init__.py,sha256=QjwaamJIdW7voXoS880yAx7tHm-Im4_mqGA-XJgafos,7631
|
|
2
|
-
sqlmesh/_version.py,sha256=
|
|
2
|
+
sqlmesh/_version.py,sha256=46P0A4hbz6tPxXHU78sPmL24cECOw9bp_3B8jB9kTr8,528
|
|
3
3
|
sqlmesh/magics.py,sha256=fu7-OeT_0_TAkWZOLoiioE3-3iFpvTRxFCbQUlRnfuw,39944
|
|
4
4
|
sqlmesh/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
5
|
sqlmesh/cicd/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
@@ -76,10 +76,11 @@ sqlmesh/core/engine_adapter/snowflake.py,sha256=oaZxKPmPXxmr46NQfw6OI5vPwvaYJ3Ym
|
|
|
76
76
|
sqlmesh/core/engine_adapter/spark.py,sha256=sYkVOPS0X43UPbI0z7Hh_EpKlI36qkOcr4NNFOTQlLs,21045
|
|
77
77
|
sqlmesh/core/engine_adapter/trino.py,sha256=7WdLFlT0AyD5NF0EDP7GqX0SHF8sTm7cafBZxk84cmY,15798
|
|
78
78
|
sqlmesh/core/linter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
79
|
-
sqlmesh/core/linter/definition.py,sha256=
|
|
80
|
-
sqlmesh/core/linter/
|
|
79
|
+
sqlmesh/core/linter/definition.py,sha256=Rjqvto7VvVEnMXXOH4q3Jol1HnPe6I6BCHK1sEJm50I,5430
|
|
80
|
+
sqlmesh/core/linter/helpers.py,sha256=VF4hsdzGZ8srpbtCjdRN_Hd8S_er5KTAaLXLde27Yxo,4032
|
|
81
|
+
sqlmesh/core/linter/rule.py,sha256=iCmFUw6Qx7GuUwLKIpHrHdq3kgtgm5JC2t9VFtMY3BQ,3180
|
|
81
82
|
sqlmesh/core/linter/rules/__init__.py,sha256=gevzfb67vFqckTCoVAe_TBGf6hQ-YtE1_YuGuXyh1L0,77
|
|
82
|
-
sqlmesh/core/linter/rules/builtin.py,sha256=
|
|
83
|
+
sqlmesh/core/linter/rules/builtin.py,sha256=EHTd4sgHT6oT93onK7t5pLmv2KgzZ81twkTnOrMi9tI,2681
|
|
83
84
|
sqlmesh/core/metric/__init__.py,sha256=H1HmoD5IwN4YWe9iJXyueLYNmTQFZwok5nSWNJcZIBQ,237
|
|
84
85
|
sqlmesh/core/metric/definition.py,sha256=Yd5aVgsZCDPJ43aGP7WqtzZOuuSUtB8uJGVA6Jw9x9M,7201
|
|
85
86
|
sqlmesh/core/metric/rewriter.py,sha256=GiSTHfn2kinqCfNPYgZPRk93JFLzVaaejHtHDQ0yXZI,7326
|
|
@@ -159,11 +160,12 @@ sqlmesh/integrations/github/cicd/config.py,sha256=tRhMZaLf1p6AWXvi67n85Q-lptJNGt
|
|
|
159
160
|
sqlmesh/integrations/github/cicd/controller.py,sha256=IgUWK9orYSMiElO3GaL6Ja-osFgA7je3RDcLChWUi5s,43769
|
|
160
161
|
sqlmesh/lsp/api.py,sha256=LJdECSaEZoPbu_Xl4KscVG-WLt7_g4dY-zKGO68dH14,2162
|
|
161
162
|
sqlmesh/lsp/completions.py,sha256=Y6zNKNne91tzRm5vRk82JmhIpo7dIBUoK8GGQesGlT8,5746
|
|
162
|
-
sqlmesh/lsp/context.py,sha256=
|
|
163
|
+
sqlmesh/lsp/context.py,sha256=36bUTiMSyfX91bri_F1aivDvSllq0rKidIM_RGAtirs,6253
|
|
163
164
|
sqlmesh/lsp/custom.py,sha256=Mml6V1nYAHdP4Zt8gxly1ZLKYALtxGeAVzPrrYpc7m8,1426
|
|
164
165
|
sqlmesh/lsp/description.py,sha256=4QIGC9ix8IzmWF6DVUNhY4m1Crk3sNQA6hZ1RbyqU78,827
|
|
165
|
-
sqlmesh/lsp/
|
|
166
|
-
sqlmesh/lsp/
|
|
166
|
+
sqlmesh/lsp/hints.py,sha256=j-blccBUil94yRoDjWfkSM0HqMmsKD6hP2TaWdWWVk4,3734
|
|
167
|
+
sqlmesh/lsp/main.py,sha256=c1h1hWluBixEFZEX2jiBjA_EOgguruaG9cSLjCrEPr8,33581
|
|
168
|
+
sqlmesh/lsp/reference.py,sha256=dyeacD_MHATGRIrl8Ax86nDK9EC_dh3TaaVNAepPce0,17902
|
|
167
169
|
sqlmesh/lsp/uri.py,sha256=mvSLbZGmMTHPpliThOtVWiFcHnfJzp1w39q9l-xD2SI,802
|
|
168
170
|
sqlmesh/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
169
171
|
sqlmesh/migrations/v0001_init.py,sha256=h8FwMYJj9_O8yGpk12guaSaGwX5A3we2GiKeHrXi15o,2048
|
|
@@ -272,7 +274,7 @@ sqlmesh/utils/pydantic.py,sha256=I898jAFqa3qU6lG2uPpRebVFncOUo8ZrLWM_R3K3DHU,116
|
|
|
272
274
|
sqlmesh/utils/rich.py,sha256=xNZyPCqEc4TAlJlsEthg-5eYUWpaizz0QjBzO7a1Yv4,1509
|
|
273
275
|
sqlmesh/utils/windows.py,sha256=r7Yin4WEwbvH7sKA3hOJtRsclmscNuBeCklCfVPvtQs,611
|
|
274
276
|
sqlmesh/utils/yaml.py,sha256=np-WVcwmwsKghCgAj1OBBMsK1SXSRn4wR-H37cGiFr8,2282
|
|
275
|
-
sqlmesh-0.189.
|
|
277
|
+
sqlmesh-0.189.3.dev1.dist-info/licenses/LICENSE,sha256=OlMefUjgWJdULtf84BLW0AZZcY8DwdgQqb_1j2862j8,11346
|
|
276
278
|
web/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
277
279
|
web/client/dist/index.html,sha256=LbR9XrOXE77otBPoDnaomssYz6h2TBwLANs6hUJ0p2M,5472
|
|
278
280
|
web/client/dist/assets/Audits-DB1HcP9M.js,sha256=eGI_Soj69Jntsixw1NqQpdwG94N5jipQZv7T1Y7WUSs,1041
|
|
@@ -390,8 +392,8 @@ web/server/api/endpoints/models.py,sha256=ud-lRwriyI_OYg9tZchrBgeiXELiRTLNEodraf
|
|
|
390
392
|
web/server/api/endpoints/modules.py,sha256=8hqqgonGay_mJmpCw0IdbjsPhWlQH2VLdKAqha-myac,468
|
|
391
393
|
web/server/api/endpoints/plan.py,sha256=OYuQ_4dgbhG9ibTcX1a8mvYgH3ANxw9Fhbb4wg6QWqc,9222
|
|
392
394
|
web/server/api/endpoints/table_diff.py,sha256=SCy3-pr5x6bEEiOaZGCDbiHQhpgxwmnz3Acqa2Ie6jI,2090
|
|
393
|
-
sqlmesh-0.189.
|
|
394
|
-
sqlmesh-0.189.
|
|
395
|
-
sqlmesh-0.189.
|
|
396
|
-
sqlmesh-0.189.
|
|
397
|
-
sqlmesh-0.189.
|
|
395
|
+
sqlmesh-0.189.3.dev1.dist-info/METADATA,sha256=S9cWZvOMCb4TkPpsuwJOjHs_Bcbiddm0657q1eWV8jE,26540
|
|
396
|
+
sqlmesh-0.189.3.dev1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
397
|
+
sqlmesh-0.189.3.dev1.dist-info/entry_points.txt,sha256=pIYKtg-u-uO7BK_Y0qrdmfyj0bYrcMc3Xiqr5WE3VFk,121
|
|
398
|
+
sqlmesh-0.189.3.dev1.dist-info/top_level.txt,sha256=bspjo5vjELQkuN0qKuTFIbsrUt3Zeo8D2mlKeHG1LdE,12
|
|
399
|
+
sqlmesh-0.189.3.dev1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|