sqlmesh 0.189.2.dev4__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 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.2.dev4'
21
- __version_tuple__ = version_tuple = (0, 189, 2, 'dev4')
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)
@@ -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(self, violation_msg: t.Optional[str] = None) -> RuleViolation:
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(rule=self, violation_msg=violation_msg or self.summary)
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__(self, rule: Rule, violation_msg: str) -> None:
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.rule import Rule, RuleViolation
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
- return self.violation() if model.query.is_star else None
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 RenderModelEntry, ModelForRendering
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,9 +36,8 @@ from sqlmesh.lsp.custom import (
36
36
  RenderModelRequest,
37
37
  RenderModelResponse,
38
38
  )
39
- from sqlmesh.lsp.reference import (
40
- get_references,
41
- )
39
+ from sqlmesh.lsp.hints import get_hints
40
+ from sqlmesh.lsp.reference import get_references, get_cte_references
42
41
  from sqlmesh.lsp.uri import URI
43
42
  from web.server.api.endpoints.lineage import column_lineage, model_lineage
44
43
  from web.server.api.endpoints.models import get_models
@@ -336,6 +335,25 @@ class SQLMeshLanguageServer:
336
335
  )
337
336
  return None
338
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
+
339
357
  @self.server.feature(types.TEXT_DOCUMENT_DEFINITION)
340
358
  def goto_definition(
341
359
  ls: LanguageServer, params: types.DefinitionParams
@@ -378,6 +396,28 @@ class SQLMeshLanguageServer:
378
396
  ls.show_message(f"Error getting references: {e}", types.MessageType.Error)
379
397
  return []
380
398
 
399
+ @self.server.feature(types.TEXT_DOCUMENT_REFERENCES)
400
+ def find_references(
401
+ ls: LanguageServer, params: types.ReferenceParams
402
+ ) -> t.Optional[t.List[types.Location]]:
403
+ """Find all references of a symbol (currently supporting CTEs)"""
404
+ try:
405
+ uri = URI(params.text_document.uri)
406
+ self._ensure_context_for_document(uri)
407
+ document = ls.workspace.get_text_document(params.text_document.uri)
408
+ if self.lsp_context is None:
409
+ raise RuntimeError(f"No context found for document: {document.path}")
410
+
411
+ cte_references = get_cte_references(self.lsp_context, uri, params.position)
412
+
413
+ # Convert references to Location objects
414
+ locations = [types.Location(uri=ref.uri, range=ref.range) for ref in cte_references]
415
+
416
+ return locations if locations else None
417
+ except Exception as e:
418
+ ls.show_message(f"Error getting locations: {e}", types.MessageType.Error)
419
+ return None
420
+
381
421
  @self.server.feature(types.TEXT_DOCUMENT_DIAGNOSTIC)
382
422
  def diagnostic(
383
423
  ls: LanguageServer, params: types.DocumentDiagnosticParams
@@ -635,8 +675,24 @@ class SQLMeshLanguageServer:
635
675
  ) -> t.Optional[types.Diagnostic]:
636
676
  if diagnostic.model._path is None:
637
677
  return None
638
- with open(diagnostic.model._path, "r", encoding="utf-8") as file:
639
- lines = file.readlines()
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
+ )
640
696
 
641
697
  # Get rule definition location for diagnostics link
642
698
  rule_location = diagnostic.rule.get_definition_location()
@@ -645,10 +701,7 @@ class SQLMeshLanguageServer:
645
701
 
646
702
  # Use URI format to create a link for "related information"
647
703
  return types.Diagnostic(
648
- range=types.Range(
649
- start=types.Position(line=0, character=0),
650
- end=types.Position(line=len(lines), character=len(lines[-1])),
651
- ),
704
+ range=range,
652
705
  message=diagnostic.violation_msg,
653
706
  severity=types.DiagnosticSeverity.Error
654
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
@@ -47,16 +52,7 @@ def by_position(position: Position) -> t.Callable[[Reference], bool]:
47
52
  """
48
53
 
49
54
  def contains_position(r: Reference) -> bool:
50
- return (
51
- r.range.start.line < position.line
52
- or (
53
- r.range.start.line == position.line
54
- and r.range.start.character <= position.character
55
- )
56
- ) and (
57
- r.range.end.line > position.line
58
- or (r.range.end.line == position.line and r.range.end.character >= position.character)
59
- )
55
+ return _position_within_range(position, r.range)
60
56
 
61
57
  return contains_position
62
58
 
@@ -165,12 +161,17 @@ def get_model_definitions_for_a_path(
165
161
  if isinstance(alias, exp.TableAlias):
166
162
  identifier = alias.this
167
163
  if isinstance(identifier, exp.Identifier):
168
- target_range = _range_from_token_position_details(
169
- TokenPositionDetails.from_meta(identifier.meta), read_file
170
- )
171
- table_range = _range_from_token_position_details(
172
- TokenPositionDetails.from_meta(table.this.meta), read_file
173
- )
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
+
174
175
  references.append(
175
176
  Reference(
176
177
  uri=document_uri.value, # Same file
@@ -212,25 +213,26 @@ def get_model_definitions_for_a_path(
212
213
 
213
214
  # Extract metadata for positioning
214
215
  table_meta = TokenPositionDetails.from_meta(table.this.meta)
215
- table_range = _range_from_token_position_details(table_meta, read_file)
216
- start_pos = table_range.start
217
- end_pos = table_range.end
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
218
219
 
219
220
  # If there's a catalog or database qualifier, adjust the start position
220
221
  catalog_or_db = table.args.get("catalog") or table.args.get("db")
221
222
  if catalog_or_db is not None:
222
223
  catalog_or_db_meta = TokenPositionDetails.from_meta(catalog_or_db.meta)
223
- catalog_or_db_range = _range_from_token_position_details(
224
- catalog_or_db_meta, read_file
225
- )
226
- 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
227
226
 
228
227
  description = generate_markdown_description(referenced_model)
229
228
 
230
229
  references.append(
231
230
  Reference(
232
231
  uri=referenced_model_uri.value,
233
- range=Range(start=start_pos, end=end_pos),
232
+ range=Range(
233
+ start=to_lsp_position(start_pos_sqlmesh),
234
+ end=to_lsp_position(end_pos_sqlmesh),
235
+ ),
234
236
  markdown_description=description,
235
237
  )
236
238
  )
@@ -238,71 +240,6 @@ def get_model_definitions_for_a_path(
238
240
  return references
239
241
 
240
242
 
241
- class TokenPositionDetails(PydanticModel):
242
- """
243
- Details about a token's position in the source code.
244
-
245
- Attributes:
246
- line (int): The line that the token ends on.
247
- col (int): The column that the token ends on.
248
- start (int): The start index of the token.
249
- end (int): The ending index of the token.
250
- """
251
-
252
- line: int
253
- col: int
254
- start: int
255
- end: int
256
-
257
- @staticmethod
258
- def from_meta(meta: t.Dict[str, int]) -> "TokenPositionDetails":
259
- return TokenPositionDetails(
260
- line=meta["line"],
261
- col=meta["col"],
262
- start=meta["start"],
263
- end=meta["end"],
264
- )
265
-
266
-
267
- def _range_from_token_position_details(
268
- token_position_details: TokenPositionDetails, read_file: t.List[str]
269
- ) -> Range:
270
- """
271
- Convert a TokenPositionDetails object to a Range object.
272
-
273
- :param token_position_details: Details about a token's position
274
- :param read_file: List of lines from the file
275
- :return: A Range object representing the token's position
276
- """
277
- # Convert from 1-indexed to 0-indexed for line only
278
- end_line_0 = token_position_details.line - 1
279
- end_col_0 = token_position_details.col
280
-
281
- # Find the start line and column by counting backwards from the end position
282
- start_pos = token_position_details.start
283
- end_pos = token_position_details.end
284
-
285
- # Initialize with the end position
286
- start_line_0 = end_line_0
287
- start_col_0 = end_col_0 - (end_pos - start_pos + 1)
288
-
289
- # If start_col_0 is negative, we need to go back to previous lines
290
- while start_col_0 < 0 and start_line_0 > 0:
291
- start_line_0 -= 1
292
- start_col_0 += len(read_file[start_line_0])
293
- # Account for newline character
294
- if start_col_0 >= 0:
295
- break
296
- start_col_0 += 1 # For the newline character
297
-
298
- # Ensure we don't have negative values
299
- start_col_0 = max(0, start_col_0)
300
- return Range(
301
- start=Position(line=start_line_0, character=start_col_0),
302
- end=Position(line=end_line_0, character=end_col_0),
303
- )
304
-
305
-
306
243
  def get_macro_definitions_for_a_path(
307
244
  lsp_context: LSPContext, document_uri: URI
308
245
  ) -> t.List[Reference]:
@@ -382,11 +319,10 @@ def get_macro_reference(
382
319
  try:
383
320
  # Get the position of the macro invocation in the source file first
384
321
  if hasattr(node, "meta") and node.meta:
385
- token_details = TokenPositionDetails.from_meta(node.meta)
386
- macro_range = _range_from_token_position_details(token_details, read_file)
322
+ macro_range = TokenPositionDetails.from_meta(node.meta).to_range(read_file)
387
323
 
388
324
  # Check if it's a built-in method
389
- 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)):
390
326
  return builtin
391
327
  else:
392
328
  # Skip if we can't get the position
@@ -438,7 +374,7 @@ def get_macro_reference(
438
374
 
439
375
  return Reference(
440
376
  uri=macro_uri.value,
441
- range=macro_range,
377
+ range=to_lsp_range(macro_range),
442
378
  target_range=Range(
443
379
  start=Position(line=start_line - 1, character=0),
444
380
  end=Position(line=end_line - 1, character=get_length_of_end_line),
@@ -478,3 +414,99 @@ def get_built_in_macro_reference(macro_name: str, macro_range: Range) -> t.Optio
478
414
  ),
479
415
  markdown_description=func.__doc__ if func.__doc__ else None,
480
416
  )
417
+
418
+
419
+ def get_cte_references(
420
+ lint_context: LSPContext, document_uri: URI, position: Position
421
+ ) -> t.List[Reference]:
422
+ """
423
+ Get all references to a CTE at a specific position in a document.
424
+
425
+ This function finds both the definition and all usages of a CTE within the same file.
426
+
427
+ Args:
428
+ lint_context: The LSP context
429
+ document_uri: The URI of the document
430
+ position: The position to check for CTE references
431
+
432
+ Returns:
433
+ A list of references to the CTE (including its definition and all usages)
434
+ """
435
+ references = get_model_definitions_for_a_path(lint_context, document_uri)
436
+
437
+ # Filter for CTE references (those with target_range set and same URI)
438
+ # TODO: Consider extending Reference class to explicitly indicate reference type instead
439
+ cte_references = [
440
+ ref for ref in references if ref.target_range is not None and ref.uri == document_uri.value
441
+ ]
442
+
443
+ if not cte_references:
444
+ return []
445
+
446
+ target_cte_definition_range = None
447
+ for ref in cte_references:
448
+ # Check if cursor is on a CTE usage
449
+ if _position_within_range(position, ref.range):
450
+ target_cte_definition_range = ref.target_range
451
+ break
452
+ # Check if cursor is on the CTE definition
453
+ elif ref.target_range and _position_within_range(position, ref.target_range):
454
+ target_cte_definition_range = ref.target_range
455
+ break
456
+
457
+ if target_cte_definition_range is None:
458
+ return []
459
+
460
+ # Add the CTE definition
461
+ matching_references = [
462
+ Reference(
463
+ uri=document_uri.value,
464
+ range=target_cte_definition_range,
465
+ markdown_description="CTE definition",
466
+ )
467
+ ]
468
+
469
+ # Add all usages
470
+ for ref in cte_references:
471
+ if ref.target_range == target_cte_definition_range:
472
+ matching_references.append(
473
+ Reference(
474
+ uri=document_uri.value,
475
+ range=ref.range,
476
+ markdown_description="CTE usage",
477
+ )
478
+ )
479
+
480
+ return matching_references
481
+
482
+
483
+ def _position_within_range(position: Position, range: Range) -> bool:
484
+ """Check if a position is within a given range."""
485
+ return (
486
+ range.start.line < position.line
487
+ or (range.start.line == position.line and range.start.character <= position.character)
488
+ ) and (
489
+ range.end.line > position.line
490
+ or (range.end.line == position.line and range.end.character >= position.character)
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sqlmesh
3
- Version: 0.189.2.dev4
3
+ Version: 0.189.3.dev1
4
4
  Summary: Next-generation data transformation framework
5
5
  Author-email: "TobikoData Inc." <engineering@tobikodata.com>
6
6
  License: Apache License
@@ -1,5 +1,5 @@
1
1
  sqlmesh/__init__.py,sha256=QjwaamJIdW7voXoS880yAx7tHm-Im4_mqGA-XJgafos,7631
2
- sqlmesh/_version.py,sha256=gPn7f-b1_vQBeafN1P-jTVGz6Eyorz0_SbXFkp6VS2Q,528
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=UL05OjFBfoLSYGWXNf4zjZYzIKr-OyFIMIcIh7RhR8s,5237
80
- sqlmesh/core/linter/rule.py,sha256=p8rbMbf2Z18ZJ6ZcZyoCk-3Lvo7d_Gvc35tiwWXvuQs,2579
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=RXns3e042q8aM-3wDUNDc7dcX5hDiOO7qel1YnjeqUk,1950
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=RVFGjKWv1o509su80izZL-Wqy8vWElqUDnv2XWvBsgs,6271
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/main.py,sha256=LwNdy-0m_CldUBlDdZv49cgZQTfjTwZQGBC3U5-Iln4,31141
166
- sqlmesh/lsp/reference.py,sha256=-HkMMzRzq4ike5mSkoWajanYNMAwhQhlPaqw62451ac,16887
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.2.dev4.dist-info/licenses/LICENSE,sha256=OlMefUjgWJdULtf84BLW0AZZcY8DwdgQqb_1j2862j8,11346
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.2.dev4.dist-info/METADATA,sha256=btn7l22yJ9q-XFDTTNEUx5o5lpIhqvTef4lXTs6vhDE,26540
394
- sqlmesh-0.189.2.dev4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
395
- sqlmesh-0.189.2.dev4.dist-info/entry_points.txt,sha256=pIYKtg-u-uO7BK_Y0qrdmfyj0bYrcMc3Xiqr5WE3VFk,121
396
- sqlmesh-0.189.2.dev4.dist-info/top_level.txt,sha256=bspjo5vjELQkuN0qKuTFIbsrUt3Zeo8D2mlKeHG1LdE,12
397
- sqlmesh-0.189.2.dev4.dist-info/RECORD,,
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,,