sqlmesh 0.187.6.dev5__py3-none-any.whl → 0.187.6.dev10__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/context.py +8 -5
- sqlmesh/core/selector.py +1 -1
- sqlmesh/lsp/main.py +178 -33
- sqlmesh/lsp/reference.py +194 -3
- {sqlmesh-0.187.6.dev5.dist-info → sqlmesh-0.187.6.dev10.dist-info}/METADATA +1 -1
- {sqlmesh-0.187.6.dev5.dist-info → sqlmesh-0.187.6.dev10.dist-info}/RECORD +56 -56
- web/client/dist/assets/{Audits-BPlxzqeG.js → Audits-C6gUfP0Y.js} +1 -1
- web/client/dist/assets/{Banner-BnIDM-p-.js → Banner-_C9c6nFf.js} +1 -1
- web/client/dist/assets/{ChevronDownIcon-v5uzV5KS.js → ChevronDownIcon-RfKS8PKd.js} +1 -1
- web/client/dist/assets/{ChevronRightIcon-lNBkq9fQ.js → ChevronRightIcon-CTOwDuap.js} +1 -1
- web/client/dist/assets/{Content-wP7OzMQ9.js → Content-Dxw_m-3F.js} +1 -1
- web/client/dist/assets/{Content-Dvky48vX.js → Content-Uj95lQQ-.js} +1 -1
- web/client/dist/assets/{Data-D2tj5W3z.js → Data-CH4v_k47.js} +1 -1
- web/client/dist/assets/{DataCatalog-H5ANpnSz.js → DataCatalog-CZgWBSbO.js} +1 -1
- web/client/dist/assets/{Editor-Dq1m6nff.js → Editor-CPGKdIv3.js} +1 -1
- web/client/dist/assets/{Editor-BpeAbLta.js → Editor-DDzPd4K5.js} +1 -1
- web/client/dist/assets/{Errors-BDlLsOz8.js → Errors-Bd_lCqWq.js} +1 -1
- web/client/dist/assets/{FileExplorer-BLOsvw4r.js → FileExplorer-FTqyN550.js} +1 -1
- web/client/dist/assets/{Footer-B4UOL2Zx.js → Footer-By8eV-Xw.js} +1 -1
- web/client/dist/assets/{Header-KC58-CQw.js → Header-CWgMNmyS.js} +1 -1
- web/client/dist/assets/{Input-DQ3H5Jf5.js → Input-tTLAeFk_.js} +1 -1
- web/client/dist/assets/Lineage-B24dFVy0.js +1 -0
- web/client/dist/assets/{ListboxShow-Bp0ARqZ0.js → ListboxShow-BnVk8Pao.js} +1 -1
- web/client/dist/assets/{ModelLineage-Du6fUL20.js → ModelLineage-CfoiOAVb.js} +1 -1
- web/client/dist/assets/{Models-BqQSh5xi.js → Models-C0WhiYIC.js} +1 -1
- web/client/dist/assets/{Page-C9v-mVEC.js → Page-LHbK1yD_.js} +1 -1
- web/client/dist/assets/{Plan-zagXv_gS.js → Plan-MwkSS6xa.js} +1 -1
- web/client/dist/assets/{PlusCircleIcon-DgugPkXr.js → PlusCircleIcon-CPdy9bpf.js} +1 -1
- web/client/dist/assets/{ReportErrors-ClY5ezEU.js → ReportErrors-9LAVRQKL.js} +1 -1
- web/client/dist/assets/{Root-BnlPjbrk.js → Root-CpkGXO_A.js} +1 -1
- web/client/dist/assets/{SearchList-B1ophTod.js → SearchList-DCuxxaPn.js} +1 -1
- web/client/dist/assets/{SelectEnvironment-CheqF0wg.js → SelectEnvironment-CltENlzE.js} +1 -1
- web/client/dist/assets/{SourceList-C_z4JbJU.js → SourceList-u_0soueL.js} +1 -1
- web/client/dist/assets/{SourceListItem-Be4vNQZL.js → SourceListItem-CHxTMFnQ.js} +1 -1
- web/client/dist/assets/{SplitPane-C8CPNQrK.js → SplitPane-DO1JEVxf.js} +1 -1
- web/client/dist/assets/{Tests-DuGqAE7Q.js → Tests-D_-8MKq9.js} +1 -1
- web/client/dist/assets/{Welcome-BovgrYsq.js → Welcome-DTulZMrl.js} +1 -1
- web/client/dist/assets/{context-BJbxiwu1.js → context-B7_ioojx.js} +1 -1
- web/client/dist/assets/{context-C7kvMRgQ.js → context-ByRTRbGV.js} +30 -30
- web/client/dist/assets/{editor-C3xrJUHo.js → editor-IxXTso9p.js} +1 -1
- web/client/dist/assets/{file-Cn8UMGdB.js → file-CtGXXxxH.js} +1 -1
- web/client/dist/assets/{floating-ui.react-dom-njuatPE_.js → floating-ui.react-dom-BqmCcnHz.js} +1 -1
- web/client/dist/assets/{help-B9pKFCi-.js → help-DZgsqPrY.js} +1 -1
- web/client/dist/assets/{index-D0B238GC.js → index-BOoFTMqX.js} +8 -8
- web/client/dist/assets/{index-0HTVGXbP.js → index-D-NV5rsk.js} +1 -1
- web/client/dist/assets/{plan-BT_YMvAu.js → plan-BITq8bOv.js} +1 -1
- web/client/dist/assets/{popover-zR5tE2ci.js → popover-CThXyzI8.js} +1 -1
- web/client/dist/assets/{project-8LRoNrpG.js → project-D9WffyNb.js} +1 -1
- web/client/dist/index.html +1 -1
- web/server/api/endpoints/files.py +1 -1
- web/server/utils.py +1 -2
- web/client/dist/assets/Lineage-COIwTlx7.js +0 -1
- {sqlmesh-0.187.6.dev5.dist-info → sqlmesh-0.187.6.dev10.dist-info}/WHEEL +0 -0
- {sqlmesh-0.187.6.dev5.dist-info → sqlmesh-0.187.6.dev10.dist-info}/entry_points.txt +0 -0
- {sqlmesh-0.187.6.dev5.dist-info → sqlmesh-0.187.6.dev10.dist-info}/licenses/LICENSE +0 -0
- {sqlmesh-0.187.6.dev5.dist-info → sqlmesh-0.187.6.dev10.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.187.6.
|
|
21
|
-
__version_tuple__ = version_tuple = (0, 187, 6, '
|
|
20
|
+
__version__ = version = '0.187.6.dev10'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 187, 6, 'dev10')
|
sqlmesh/core/context.py
CHANGED
|
@@ -932,19 +932,22 @@ class GenericContext(BaseContext, t.Generic[C]):
|
|
|
932
932
|
|
|
933
933
|
return snapshot
|
|
934
934
|
|
|
935
|
-
def config_for_path(self, path: Path) -> Config:
|
|
935
|
+
def config_for_path(self, path: Path) -> t.Tuple[Config, Path]:
|
|
936
|
+
"""Returns the config and path of the said project for a given file path."""
|
|
936
937
|
for config_path, config in self.configs.items():
|
|
937
938
|
try:
|
|
938
939
|
path.relative_to(config_path)
|
|
939
|
-
return config
|
|
940
|
+
return config, config_path
|
|
940
941
|
except ValueError:
|
|
941
942
|
pass
|
|
942
|
-
return self.config
|
|
943
|
+
return self.config, self.path
|
|
943
944
|
|
|
944
945
|
def config_for_node(self, node: str | Model | Audit) -> Config:
|
|
945
946
|
if isinstance(node, str):
|
|
946
|
-
return self.config_for_path(self.get_snapshot(node, raise_if_missing=True).node._path)
|
|
947
|
-
|
|
947
|
+
return self.config_for_path(self.get_snapshot(node, raise_if_missing=True).node._path)[
|
|
948
|
+
0
|
|
949
|
+
] # type: ignore
|
|
950
|
+
return self.config_for_path(node._path)[0] # type: ignore
|
|
948
951
|
|
|
949
952
|
@property
|
|
950
953
|
def models(self) -> MappingProxyType[str, Model]:
|
sqlmesh/core/selector.py
CHANGED
sqlmesh/lsp/main.py
CHANGED
|
@@ -57,8 +57,12 @@ class SQLMeshLanguageServer:
|
|
|
57
57
|
self.server = LanguageServer(server_name, version)
|
|
58
58
|
self.context_class = context_class
|
|
59
59
|
self.lsp_context: t.Optional[LSPContext] = None
|
|
60
|
-
self.lint_cache: t.Dict[URI, t.List[AnnotatedRuleViolation]] = {}
|
|
61
60
|
|
|
61
|
+
# Cache stores tuples of (diagnostics, diagnostic_version)
|
|
62
|
+
self.lint_cache: t.Dict[URI, t.Tuple[t.List[AnnotatedRuleViolation], int]] = {}
|
|
63
|
+
self._diagnostic_version_counter: int = 0
|
|
64
|
+
|
|
65
|
+
self.client_supports_pull_diagnostics = False
|
|
62
66
|
# Register LSP features (e.g., formatting, hover, etc.)
|
|
63
67
|
self._register_features()
|
|
64
68
|
|
|
@@ -69,6 +73,18 @@ class SQLMeshLanguageServer:
|
|
|
69
73
|
def initialize(ls: LanguageServer, params: types.InitializeParams) -> None:
|
|
70
74
|
"""Initialize the server when the client connects."""
|
|
71
75
|
try:
|
|
76
|
+
# Check if client supports pull diagnostics
|
|
77
|
+
if params.capabilities and params.capabilities.text_document:
|
|
78
|
+
diagnostics = getattr(params.capabilities.text_document, "diagnostic", None)
|
|
79
|
+
if diagnostics:
|
|
80
|
+
self.client_supports_pull_diagnostics = True
|
|
81
|
+
ls.log_trace("Client supports pull diagnostics")
|
|
82
|
+
else:
|
|
83
|
+
self.client_supports_pull_diagnostics = False
|
|
84
|
+
ls.log_trace("Client does not support pull diagnostics")
|
|
85
|
+
else:
|
|
86
|
+
self.client_supports_pull_diagnostics = False
|
|
87
|
+
|
|
72
88
|
if params.workspace_folders:
|
|
73
89
|
# Try to find a SQLMesh config file in any workspace folder (only at the root level)
|
|
74
90
|
for folder in params.workspace_folders:
|
|
@@ -153,61 +169,71 @@ class SQLMeshLanguageServer:
|
|
|
153
169
|
def did_open(ls: LanguageServer, params: types.DidOpenTextDocumentParams) -> None:
|
|
154
170
|
uri = URI(params.text_document.uri)
|
|
155
171
|
context = self._context_get_or_load(uri)
|
|
156
|
-
|
|
172
|
+
models = context.map[uri.to_path()]
|
|
173
|
+
if models is None or not isinstance(models, ModelTarget):
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
if self.lint_cache.get(uri) is None:
|
|
177
|
+
diagnostics = context.context.lint_models(
|
|
178
|
+
models.names,
|
|
179
|
+
raise_on_error=False,
|
|
180
|
+
)
|
|
181
|
+
self._diagnostic_version_counter += 1
|
|
182
|
+
self.lint_cache[uri] = (diagnostics, self._diagnostic_version_counter)
|
|
183
|
+
|
|
184
|
+
# Only publish diagnostics if client doesn't support pull diagnostics
|
|
185
|
+
if not self.client_supports_pull_diagnostics:
|
|
186
|
+
diagnostics, _ = self.lint_cache[uri]
|
|
157
187
|
ls.publish_diagnostics(
|
|
158
188
|
params.text_document.uri,
|
|
159
|
-
SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics(
|
|
189
|
+
SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics(diagnostics),
|
|
160
190
|
)
|
|
161
|
-
return
|
|
162
|
-
models = context.map[uri.to_path()]
|
|
163
|
-
if models is None:
|
|
164
|
-
return
|
|
165
|
-
if not isinstance(models, ModelTarget):
|
|
166
|
-
return
|
|
167
|
-
self.lint_cache[uri] = context.context.lint_models(
|
|
168
|
-
models.names,
|
|
169
|
-
raise_on_error=False,
|
|
170
|
-
)
|
|
171
|
-
ls.publish_diagnostics(
|
|
172
|
-
params.text_document.uri,
|
|
173
|
-
SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics(self.lint_cache[uri]),
|
|
174
|
-
)
|
|
175
191
|
|
|
176
192
|
@self.server.feature(types.TEXT_DOCUMENT_DID_CHANGE)
|
|
177
193
|
def did_change(ls: LanguageServer, params: types.DidChangeTextDocumentParams) -> None:
|
|
178
194
|
uri = URI(params.text_document.uri)
|
|
179
195
|
context = self._context_get_or_load(uri)
|
|
180
196
|
models = context.map[uri.to_path()]
|
|
181
|
-
if models is None:
|
|
197
|
+
if models is None or not isinstance(models, ModelTarget):
|
|
182
198
|
return
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
199
|
+
|
|
200
|
+
# Always update the cache
|
|
201
|
+
diagnostics = context.context.lint_models(
|
|
186
202
|
models.names,
|
|
187
203
|
raise_on_error=False,
|
|
188
204
|
)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
205
|
+
self._diagnostic_version_counter += 1
|
|
206
|
+
self.lint_cache[uri] = (diagnostics, self._diagnostic_version_counter)
|
|
207
|
+
|
|
208
|
+
# Only publish diagnostics if client doesn't support pull diagnostics
|
|
209
|
+
if not self.client_supports_pull_diagnostics:
|
|
210
|
+
ls.publish_diagnostics(
|
|
211
|
+
params.text_document.uri,
|
|
212
|
+
SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics(diagnostics),
|
|
213
|
+
)
|
|
193
214
|
|
|
194
215
|
@self.server.feature(types.TEXT_DOCUMENT_DID_SAVE)
|
|
195
216
|
def did_save(ls: LanguageServer, params: types.DidSaveTextDocumentParams) -> None:
|
|
196
217
|
uri = URI(params.text_document.uri)
|
|
197
218
|
context = self._context_get_or_load(uri)
|
|
198
219
|
models = context.map[uri.to_path()]
|
|
199
|
-
if models is None:
|
|
200
|
-
return
|
|
201
|
-
if not isinstance(models, ModelTarget):
|
|
220
|
+
if models is None or not isinstance(models, ModelTarget):
|
|
202
221
|
return
|
|
203
|
-
|
|
222
|
+
|
|
223
|
+
# Always update the cache
|
|
224
|
+
diagnostics = context.context.lint_models(
|
|
204
225
|
models.names,
|
|
205
226
|
raise_on_error=False,
|
|
206
227
|
)
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
228
|
+
self._diagnostic_version_counter += 1
|
|
229
|
+
self.lint_cache[uri] = (diagnostics, self._diagnostic_version_counter)
|
|
230
|
+
|
|
231
|
+
# Only publish diagnostics if client doesn't support pull diagnostics
|
|
232
|
+
if not self.client_supports_pull_diagnostics:
|
|
233
|
+
ls.publish_diagnostics(
|
|
234
|
+
params.text_document.uri,
|
|
235
|
+
SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics(diagnostics),
|
|
236
|
+
)
|
|
211
237
|
|
|
212
238
|
@self.server.feature(types.TEXT_DOCUMENT_FORMATTING)
|
|
213
239
|
def formatting(
|
|
@@ -327,6 +353,125 @@ class SQLMeshLanguageServer:
|
|
|
327
353
|
ls.show_message(f"Error getting references: {e}", types.MessageType.Error)
|
|
328
354
|
return []
|
|
329
355
|
|
|
356
|
+
@self.server.feature(types.TEXT_DOCUMENT_DIAGNOSTIC)
|
|
357
|
+
def diagnostic(
|
|
358
|
+
ls: LanguageServer, params: types.DocumentDiagnosticParams
|
|
359
|
+
) -> types.DocumentDiagnosticReport:
|
|
360
|
+
"""Handle diagnostic pull requests from the client."""
|
|
361
|
+
try:
|
|
362
|
+
uri = URI(params.text_document.uri)
|
|
363
|
+
diagnostics, result_id = self._get_diagnostics_for_uri(uri)
|
|
364
|
+
|
|
365
|
+
# Check if client provided a previous result ID
|
|
366
|
+
if hasattr(params, "previous_result_id") and params.previous_result_id == result_id:
|
|
367
|
+
# Return unchanged report if diagnostics haven't changed
|
|
368
|
+
return types.RelatedUnchangedDocumentDiagnosticReport(
|
|
369
|
+
kind=types.DocumentDiagnosticReportKind.Unchanged,
|
|
370
|
+
result_id=str(result_id),
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
return types.RelatedFullDocumentDiagnosticReport(
|
|
374
|
+
kind=types.DocumentDiagnosticReportKind.Full,
|
|
375
|
+
items=diagnostics,
|
|
376
|
+
result_id=str(result_id),
|
|
377
|
+
)
|
|
378
|
+
except Exception as e:
|
|
379
|
+
ls.show_message(f"Error getting diagnostics: {e}", types.MessageType.Error)
|
|
380
|
+
return types.RelatedFullDocumentDiagnosticReport(
|
|
381
|
+
kind=types.DocumentDiagnosticReportKind.Full,
|
|
382
|
+
items=[],
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
@self.server.feature(types.WORKSPACE_DIAGNOSTIC)
|
|
386
|
+
def workspace_diagnostic(
|
|
387
|
+
ls: LanguageServer, params: types.WorkspaceDiagnosticParams
|
|
388
|
+
) -> types.WorkspaceDiagnosticReport:
|
|
389
|
+
"""Handle workspace-wide diagnostic pull requests from the client."""
|
|
390
|
+
try:
|
|
391
|
+
if self.lsp_context is None:
|
|
392
|
+
current_path = Path.cwd()
|
|
393
|
+
self._ensure_context_in_folder(current_path)
|
|
394
|
+
|
|
395
|
+
if self.lsp_context is None:
|
|
396
|
+
return types.WorkspaceDiagnosticReport(items=[])
|
|
397
|
+
|
|
398
|
+
items: t.List[
|
|
399
|
+
t.Union[
|
|
400
|
+
types.WorkspaceFullDocumentDiagnosticReport,
|
|
401
|
+
types.WorkspaceUnchangedDocumentDiagnosticReport,
|
|
402
|
+
]
|
|
403
|
+
] = []
|
|
404
|
+
|
|
405
|
+
# Get all SQL and Python model files from the context
|
|
406
|
+
for path, target in self.lsp_context.map.items():
|
|
407
|
+
if isinstance(target, ModelTarget):
|
|
408
|
+
uri = URI.from_path(path)
|
|
409
|
+
diagnostics, result_id = self._get_diagnostics_for_uri(uri)
|
|
410
|
+
|
|
411
|
+
# Check if we have a previous result ID for this file
|
|
412
|
+
previous_result_id = None
|
|
413
|
+
if hasattr(params, "previous_result_ids") and params.previous_result_ids:
|
|
414
|
+
for prev in params.previous_result_ids:
|
|
415
|
+
if prev.uri == uri.value:
|
|
416
|
+
previous_result_id = prev.value
|
|
417
|
+
break
|
|
418
|
+
|
|
419
|
+
if previous_result_id and previous_result_id == result_id:
|
|
420
|
+
# File hasn't changed
|
|
421
|
+
items.append(
|
|
422
|
+
types.WorkspaceUnchangedDocumentDiagnosticReport(
|
|
423
|
+
kind=types.DocumentDiagnosticReportKind.Unchanged,
|
|
424
|
+
result_id=str(result_id),
|
|
425
|
+
uri=uri.value,
|
|
426
|
+
)
|
|
427
|
+
)
|
|
428
|
+
else:
|
|
429
|
+
# File has changed or is new
|
|
430
|
+
items.append(
|
|
431
|
+
types.WorkspaceFullDocumentDiagnosticReport(
|
|
432
|
+
kind=types.DocumentDiagnosticReportKind.Full,
|
|
433
|
+
result_id=str(result_id),
|
|
434
|
+
uri=uri.value,
|
|
435
|
+
items=diagnostics,
|
|
436
|
+
)
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
return types.WorkspaceDiagnosticReport(items=items)
|
|
440
|
+
|
|
441
|
+
except Exception as e:
|
|
442
|
+
ls.show_message(
|
|
443
|
+
f"Error getting workspace diagnostics: {e}", types.MessageType.Error
|
|
444
|
+
)
|
|
445
|
+
return types.WorkspaceDiagnosticReport(items=[])
|
|
446
|
+
|
|
447
|
+
def _get_diagnostics_for_uri(self, uri: URI) -> t.Tuple[t.List[types.Diagnostic], int]:
|
|
448
|
+
"""Get diagnostics for a specific URI, returning (diagnostics, result_id)."""
|
|
449
|
+
# Check if we have cached diagnostics
|
|
450
|
+
if uri in self.lint_cache:
|
|
451
|
+
diagnostics, result_id = self.lint_cache[uri]
|
|
452
|
+
return SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics(diagnostics), result_id
|
|
453
|
+
|
|
454
|
+
# Try to get diagnostics by loading context and linting
|
|
455
|
+
try:
|
|
456
|
+
context = self._context_get_or_load(uri)
|
|
457
|
+
models = context.map[uri.to_path()]
|
|
458
|
+
if models is None or not isinstance(models, ModelTarget):
|
|
459
|
+
return [], 0
|
|
460
|
+
|
|
461
|
+
# Lint the models and cache the results
|
|
462
|
+
diagnostics = context.context.lint_models(
|
|
463
|
+
models.names,
|
|
464
|
+
raise_on_error=False,
|
|
465
|
+
)
|
|
466
|
+
self._diagnostic_version_counter += 1
|
|
467
|
+
self.lint_cache[uri] = (diagnostics, self._diagnostic_version_counter)
|
|
468
|
+
return SQLMeshLanguageServer._diagnostics_to_lsp_diagnostics(
|
|
469
|
+
diagnostics
|
|
470
|
+
), self._diagnostic_version_counter
|
|
471
|
+
except Exception:
|
|
472
|
+
# If we can't get diagnostics, return empty list with no result ID
|
|
473
|
+
return [], 0
|
|
474
|
+
|
|
330
475
|
def _context_get_or_load(self, document_uri: URI) -> LSPContext:
|
|
331
476
|
if self.lsp_context is None:
|
|
332
477
|
self._ensure_context_for_document(document_uri)
|
sqlmesh/lsp/reference.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from lsprotocol.types import Range, Position
|
|
2
2
|
import typing as t
|
|
3
|
+
from pathlib import Path
|
|
3
4
|
|
|
5
|
+
from sqlmesh.core.audit import StandaloneAudit
|
|
4
6
|
from sqlmesh.core.dialect import normalize_model_name
|
|
5
7
|
from sqlmesh.core.model.definition import SqlModel
|
|
6
8
|
from sqlmesh.lsp.context import LSPContext, ModelTarget, AuditTarget
|
|
@@ -10,6 +12,10 @@ from sqlglot.optimizer.scope import build_scope
|
|
|
10
12
|
from sqlmesh.lsp.uri import URI
|
|
11
13
|
from sqlmesh.utils.pydantic import PydanticModel
|
|
12
14
|
from sqlglot.optimizer.normalize_identifiers import normalize_identifiers
|
|
15
|
+
import ast
|
|
16
|
+
from sqlmesh.core.model import Model
|
|
17
|
+
from sqlmesh import macro
|
|
18
|
+
import inspect
|
|
13
19
|
|
|
14
20
|
|
|
15
21
|
class Reference(PydanticModel):
|
|
@@ -72,6 +78,11 @@ def get_references(
|
|
|
72
78
|
A list of references at the given position
|
|
73
79
|
"""
|
|
74
80
|
references = get_model_definitions_for_a_path(lint_context, document_uri)
|
|
81
|
+
|
|
82
|
+
# Get macro references before filtering by position
|
|
83
|
+
macro_references = get_macro_definitions_for_a_path(lint_context, document_uri)
|
|
84
|
+
references.extend(macro_references)
|
|
85
|
+
|
|
75
86
|
filtered_references = list(filter(by_position(position), references))
|
|
76
87
|
return filtered_references
|
|
77
88
|
|
|
@@ -134,9 +145,12 @@ def get_model_definitions_for_a_path(
|
|
|
134
145
|
with open(file_path, "r", encoding="utf-8") as file:
|
|
135
146
|
read_file = file.readlines()
|
|
136
147
|
|
|
137
|
-
# Build scope tree to properly handle nested CTEs
|
|
138
|
-
|
|
139
|
-
|
|
148
|
+
# Build a scope tree to properly handle nested CTEs
|
|
149
|
+
try:
|
|
150
|
+
query = normalize_identifiers(query.copy(), dialect=dialect)
|
|
151
|
+
root_scope = build_scope(query)
|
|
152
|
+
except Exception:
|
|
153
|
+
root_scope = None
|
|
140
154
|
|
|
141
155
|
if root_scope:
|
|
142
156
|
# Traverse all scopes to find CTE definitions and table references
|
|
@@ -287,3 +301,180 @@ def _range_from_token_position_details(
|
|
|
287
301
|
start=Position(line=start_line_0, character=start_col_0),
|
|
288
302
|
end=Position(line=end_line_0, character=end_col_0),
|
|
289
303
|
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def get_macro_definitions_for_a_path(
|
|
307
|
+
lsp_context: LSPContext, document_uri: URI
|
|
308
|
+
) -> t.List[Reference]:
|
|
309
|
+
"""
|
|
310
|
+
Get macro references for a given path.
|
|
311
|
+
|
|
312
|
+
This function finds all macro invocations (e.g., @ADD_ONE, @MULTIPLY) in a SQL file
|
|
313
|
+
and creates references to their definitions in the Python macro files.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
lsp_context: The LSP context containing macro definitions
|
|
317
|
+
document_uri: The URI of the document to search for macro invocations
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
A list of Reference objects for each macro invocation found
|
|
321
|
+
"""
|
|
322
|
+
path = document_uri.to_path()
|
|
323
|
+
if path.suffix != ".sql":
|
|
324
|
+
return []
|
|
325
|
+
|
|
326
|
+
# Get the file info from the context map
|
|
327
|
+
if path not in lsp_context.map:
|
|
328
|
+
return []
|
|
329
|
+
|
|
330
|
+
file_info = lsp_context.map[path]
|
|
331
|
+
# Process based on whether it's a model or standalone audit
|
|
332
|
+
if isinstance(file_info, ModelTarget):
|
|
333
|
+
# It's a model
|
|
334
|
+
target: t.Optional[t.Union[Model, StandaloneAudit]] = lsp_context.context.get_model(
|
|
335
|
+
model_or_snapshot=file_info.names[0], raise_if_missing=False
|
|
336
|
+
)
|
|
337
|
+
if target is None or not isinstance(target, SqlModel):
|
|
338
|
+
return []
|
|
339
|
+
query = target.query
|
|
340
|
+
file_path = target._path
|
|
341
|
+
elif isinstance(file_info, AuditTarget):
|
|
342
|
+
# It's a standalone audit
|
|
343
|
+
target = lsp_context.context.standalone_audits.get(file_info.name)
|
|
344
|
+
if target is None:
|
|
345
|
+
return []
|
|
346
|
+
query = target.query
|
|
347
|
+
file_path = target._path
|
|
348
|
+
else:
|
|
349
|
+
return []
|
|
350
|
+
|
|
351
|
+
references = []
|
|
352
|
+
config_for_model, config_path = lsp_context.context.config_for_path(
|
|
353
|
+
file_path,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
with open(file_path, "r", encoding="utf-8") as file:
|
|
357
|
+
read_file = file.readlines()
|
|
358
|
+
|
|
359
|
+
for node in query.find_all(exp.Anonymous):
|
|
360
|
+
macro_name = node.name.lower()
|
|
361
|
+
reference = get_macro_reference(
|
|
362
|
+
node=node,
|
|
363
|
+
target=target,
|
|
364
|
+
read_file=read_file,
|
|
365
|
+
config_path=config_path,
|
|
366
|
+
macro_name=macro_name,
|
|
367
|
+
)
|
|
368
|
+
if reference is not None:
|
|
369
|
+
references.append(reference)
|
|
370
|
+
|
|
371
|
+
return references
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def get_macro_reference(
|
|
375
|
+
target: t.Union[Model, StandaloneAudit],
|
|
376
|
+
read_file: t.List[str],
|
|
377
|
+
config_path: t.Optional[Path],
|
|
378
|
+
node: exp.Expression,
|
|
379
|
+
macro_name: str,
|
|
380
|
+
) -> t.Optional[Reference]:
|
|
381
|
+
# Get the file path where the macro is defined
|
|
382
|
+
try:
|
|
383
|
+
# Get the position of the macro invocation in the source file first
|
|
384
|
+
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)
|
|
387
|
+
|
|
388
|
+
# Check if it's a built-in method
|
|
389
|
+
if builtin := get_built_in_macro_reference(macro_name, macro_range):
|
|
390
|
+
return builtin
|
|
391
|
+
else:
|
|
392
|
+
# Skip if we can't get the position
|
|
393
|
+
return None
|
|
394
|
+
|
|
395
|
+
# Find the macro definition information
|
|
396
|
+
macro_def = target.python_env.get(macro_name)
|
|
397
|
+
if macro_def is None:
|
|
398
|
+
return None
|
|
399
|
+
|
|
400
|
+
function_name = macro_def.name
|
|
401
|
+
if not function_name:
|
|
402
|
+
return None
|
|
403
|
+
if not macro_def.path:
|
|
404
|
+
return None
|
|
405
|
+
if not config_path:
|
|
406
|
+
return None
|
|
407
|
+
path = Path(config_path).joinpath(macro_def.path)
|
|
408
|
+
|
|
409
|
+
# Parse the Python file to find the function definition
|
|
410
|
+
with open(path, "r") as f:
|
|
411
|
+
tree = ast.parse(f.read())
|
|
412
|
+
with open(path, "r") as f:
|
|
413
|
+
output_read_line = f.readlines()
|
|
414
|
+
|
|
415
|
+
# Find the function definition by name
|
|
416
|
+
start_line = None
|
|
417
|
+
end_line = None
|
|
418
|
+
get_length_of_end_line = None
|
|
419
|
+
docstring = None
|
|
420
|
+
for ast_node in ast.walk(tree):
|
|
421
|
+
if isinstance(ast_node, ast.FunctionDef) and ast_node.name == function_name:
|
|
422
|
+
start_line = ast_node.lineno
|
|
423
|
+
end_line = ast_node.end_lineno
|
|
424
|
+
get_length_of_end_line = (
|
|
425
|
+
len(output_read_line[end_line - 1])
|
|
426
|
+
if end_line is not None and end_line - 1 < len(read_file)
|
|
427
|
+
else 0
|
|
428
|
+
)
|
|
429
|
+
# Extract docstring if present
|
|
430
|
+
docstring = ast.get_docstring(ast_node)
|
|
431
|
+
break
|
|
432
|
+
|
|
433
|
+
if start_line is None or end_line is None or get_length_of_end_line is None:
|
|
434
|
+
return None
|
|
435
|
+
|
|
436
|
+
# Create a reference to the macro definition
|
|
437
|
+
macro_uri = URI.from_path(path)
|
|
438
|
+
|
|
439
|
+
return Reference(
|
|
440
|
+
uri=macro_uri.value,
|
|
441
|
+
range=macro_range,
|
|
442
|
+
target_range=Range(
|
|
443
|
+
start=Position(line=start_line - 1, character=0),
|
|
444
|
+
end=Position(line=end_line - 1, character=get_length_of_end_line),
|
|
445
|
+
),
|
|
446
|
+
markdown_description=docstring,
|
|
447
|
+
)
|
|
448
|
+
except Exception:
|
|
449
|
+
return None
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def get_built_in_macro_reference(macro_name: str, macro_range: Range) -> t.Optional[Reference]:
|
|
453
|
+
"""
|
|
454
|
+
Get a reference to a built-in macro by its name.
|
|
455
|
+
|
|
456
|
+
Args:
|
|
457
|
+
macro_name: The name of the built-in macro (e.g., 'each', 'sql_literal')
|
|
458
|
+
macro_range: The range of the macro invocation in the source file
|
|
459
|
+
"""
|
|
460
|
+
built_in_macros = macro.get_registry()
|
|
461
|
+
built_in_macro = built_in_macros.get(macro_name)
|
|
462
|
+
if built_in_macro is None:
|
|
463
|
+
return None
|
|
464
|
+
|
|
465
|
+
func = built_in_macro.func
|
|
466
|
+
filename = inspect.getfile(func)
|
|
467
|
+
source_lines, line_number = inspect.getsourcelines(func)
|
|
468
|
+
|
|
469
|
+
# Calculate the end line number by counting the number of source lines
|
|
470
|
+
end_line_number = line_number + len(source_lines) - 1
|
|
471
|
+
|
|
472
|
+
return Reference(
|
|
473
|
+
uri=URI.from_path(Path(filename)).value,
|
|
474
|
+
range=macro_range,
|
|
475
|
+
target_range=Range(
|
|
476
|
+
start=Position(line=line_number - 1, character=0),
|
|
477
|
+
end=Position(line=end_line_number - 1, character=0),
|
|
478
|
+
),
|
|
479
|
+
markdown_description=func.__doc__ if func.__doc__ else None,
|
|
480
|
+
)
|