Skip to content

Commit 321b118

Browse files
authored
feat(vscode): gracefully handle not supported apis (#4678)
1 parent 53224fc commit 321b118

File tree

6 files changed

+252
-4
lines changed

6 files changed

+252
-4
lines changed

sqlmesh/lsp/custom.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,30 @@ class AllModelsForRenderResponse(PydanticModel):
7272
"""
7373

7474
models: t.List[ModelForRendering]
75+
76+
77+
SUPPORTED_METHODS_FEATURE = "sqlmesh/supported_methods"
78+
79+
80+
class SupportedMethodsRequest(PydanticModel):
81+
"""
82+
Request to get all supported custom LSP methods.
83+
"""
84+
85+
pass
86+
87+
88+
class CustomMethod(PydanticModel):
89+
"""
90+
Information about a custom LSP method.
91+
"""
92+
93+
name: str
94+
95+
96+
class SupportedMethodsResponse(PydanticModel):
97+
"""
98+
Response containing all supported custom LSP methods.
99+
"""
100+
101+
methods: t.List[CustomMethod]

sqlmesh/lsp/main.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,31 @@
2929
ALL_MODELS_FEATURE,
3030
ALL_MODELS_FOR_RENDER_FEATURE,
3131
RENDER_MODEL_FEATURE,
32+
SUPPORTED_METHODS_FEATURE,
3233
AllModelsRequest,
3334
AllModelsResponse,
3435
AllModelsForRenderRequest,
3536
AllModelsForRenderResponse,
3637
RenderModelRequest,
3738
RenderModelResponse,
39+
SupportedMethodsRequest,
40+
SupportedMethodsResponse,
41+
CustomMethod,
3842
)
3943
from sqlmesh.lsp.hints import get_hints
4044
from sqlmesh.lsp.reference import get_references, get_cte_references
4145
from sqlmesh.lsp.uri import URI
4246
from web.server.api.endpoints.lineage import column_lineage, model_lineage
4347
from web.server.api.endpoints.models import get_models
4448

49+
SUPPORTED_CUSTOM_METHODS = [
50+
ALL_MODELS_FEATURE,
51+
RENDER_MODEL_FEATURE,
52+
ALL_MODELS_FOR_RENDER_FEATURE,
53+
API_FEATURE,
54+
SUPPORTED_METHODS_FEATURE,
55+
]
56+
4557

4658
class SQLMeshLanguageServer:
4759
def __init__(
@@ -149,6 +161,20 @@ def all_models_for_render(
149161
models=self.lsp_context.list_of_models_for_rendering()
150162
)
151163

164+
@self.server.feature(SUPPORTED_METHODS_FEATURE)
165+
def supported_methods(
166+
ls: LanguageServer, params: SupportedMethodsRequest
167+
) -> SupportedMethodsResponse:
168+
"""Return all supported custom LSP methods."""
169+
return SupportedMethodsResponse(
170+
methods=[
171+
CustomMethod(
172+
name=name,
173+
)
174+
for name in SUPPORTED_CUSTOM_METHODS
175+
]
176+
)
177+
152178
@self.server.feature(API_FEATURE)
153179
def api(ls: LanguageServer, request: ApiRequest) -> t.Dict[str, t.Any]:
154180
ls.log_trace(f"API request: {request}")

vscode/extension/src/commands/renderModel.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ export function renderModel(
7373

7474
if (isErr(allModelsResult)) {
7575
vscode.window.showErrorMessage(
76-
`Failed to get models: ${allModelsResult.error}`,
76+
`Failed to get models: ${allModelsResult.error.message}`,
7777
)
7878
return
7979
}
@@ -115,7 +115,9 @@ export function renderModel(
115115
})
116116

117117
if (isErr(result)) {
118-
vscode.window.showErrorMessage(`Failed to render model: ${result.error}`)
118+
vscode.window.showErrorMessage(
119+
`Failed to render model: ${result.error.message}`,
120+
)
119121
return
120122
}
121123

vscode/extension/src/lsp/custom.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export type CustomLSPMethods =
3131
| AbstractAPICall
3232
| RenderModelMethod
3333
| AllModelsForRenderMethod
34+
| SupportedMethodsMethod
3435

3536
interface AllModelsRequest {
3637
textDocument: {
@@ -75,3 +76,20 @@ export interface ModelForRendering {
7576
description: string | null | undefined
7677
uri: string
7778
}
79+
80+
export interface SupportedMethodsMethod {
81+
method: 'sqlmesh/supported_methods'
82+
request: SupportedMethodsRequest
83+
response: SupportedMethodsResponse
84+
}
85+
86+
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
87+
interface SupportedMethodsRequest {}
88+
89+
interface SupportedMethodsResponse {
90+
methods: CustomMethod[]
91+
}
92+
93+
interface CustomMethod {
94+
name: string
95+
}

vscode/extension/src/lsp/lsp.ts

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,41 @@ import { sqlmeshLspExec } from '../utilities/sqlmesh/sqlmesh'
99
import { err, isErr, ok, Result } from '@bus/result'
1010
import { getWorkspaceFolders } from '../utilities/common/vscodeapi'
1111
import { traceError, traceInfo } from '../utilities/common/log'
12-
import { ErrorType } from '../utilities/errors'
12+
import {
13+
ErrorType,
14+
ErrorTypeGeneric,
15+
ErrorTypeInvalidState,
16+
ErrorTypeSQLMeshOutdated,
17+
} from '../utilities/errors'
1318
import { CustomLSPMethods } from './custom'
1419

20+
type SupportedMethodsState =
21+
| { type: 'not-fetched' }
22+
| { type: 'fetched'; methods: Set<string> }
23+
// TODO: This state is used when the `sqlmesh/supported_methods` endpoint is
24+
// not supported by the LSP server. This is in order to be backward compatible
25+
// with older versions of SQLMesh that do not support this endpoint. At some point
26+
// we should remove this state and always fetch the supported methods.
27+
| { type: 'endpoint-not-supported' }
28+
1529
let outputChannel: OutputChannel | undefined
1630

1731
export class LSPClient implements Disposable {
1832
private client: LanguageClient | undefined
33+
/**
34+
* State to track whether the supported methods have been fetched. These are used to determine if a method is supported
35+
* by the LSP server and return an error if not.
36+
*/
37+
private supportedMethodsState: SupportedMethodsState = { type: 'not-fetched' }
1938

2039
constructor() {
2140
this.client = undefined
2241
}
2342

43+
// TODO: This method is used to check if the LSP client has completion capability
44+
// in order to be backward compatible with older versions of SQLMesh that do not
45+
// support completion. At some point we should remove this method and always assume
46+
// that the LSP client has completion capability.
2447
public hasCompletionCapability(): boolean {
2548
if (!this.client) {
2649
traceError('LSP client is not initialized')
@@ -98,21 +121,123 @@ export class LSPClient implements Disposable {
98121
if (this.client) {
99122
await this.client.stop()
100123
this.client = undefined
124+
// Reset supported methods state when the client stops
125+
this.supportedMethodsState = { type: 'not-fetched' }
101126
}
102127
}
103128

104129
public async dispose() {
105130
await this.stop()
106131
}
107132

133+
private async fetchSupportedMethods(): Promise<void> {
134+
if (!this.client || this.supportedMethodsState.type !== 'not-fetched') {
135+
return
136+
}
137+
try {
138+
const result = await this.internal_call_custom_method(
139+
'sqlmesh/supported_methods',
140+
{},
141+
)
142+
if (isErr(result)) {
143+
traceError(`Failed to fetch supported methods: ${result.error}`)
144+
this.supportedMethodsState = { type: 'endpoint-not-supported' }
145+
return
146+
}
147+
const methodNames = new Set(result.value.methods.map(m => m.name))
148+
this.supportedMethodsState = { type: 'fetched', methods: methodNames }
149+
traceInfo(
150+
`Fetched supported methods: ${Array.from(methodNames).join(', ')}`,
151+
)
152+
} catch {
153+
// If the supported_methods endpoint doesn't exist, mark it as not supported
154+
this.supportedMethodsState = { type: 'endpoint-not-supported' }
155+
traceInfo(
156+
'Supported methods endpoint not available, proceeding without validation',
157+
)
158+
}
159+
}
160+
108161
public async call_custom_method<
162+
Method extends Exclude<
163+
CustomLSPMethods['method'],
164+
'sqlmesh/supported_methods'
165+
>,
166+
Request extends Extract<CustomLSPMethods, { method: Method }>['request'],
167+
Response extends Extract<CustomLSPMethods, { method: Method }>['response'],
168+
>(
169+
method: Method,
170+
request: Request,
171+
): Promise<
172+
Result<
173+
Response,
174+
ErrorTypeGeneric | ErrorTypeInvalidState | ErrorTypeSQLMeshOutdated
175+
>
176+
> {
177+
if (!this.client) {
178+
return err({
179+
type: 'generic',
180+
message: 'LSP client not ready.',
181+
})
182+
}
183+
await this.fetchSupportedMethods()
184+
185+
const supportedState = this.supportedMethodsState
186+
switch (supportedState.type) {
187+
case 'not-fetched':
188+
return err({
189+
type: 'invalid_state',
190+
message: 'Supported methods not fetched yet whereas they should.',
191+
})
192+
case 'fetched': {
193+
// If we have fetched the supported methods, we can check if the method is supported
194+
if (!supportedState.methods.has(method)) {
195+
return err({
196+
type: 'sqlmesh_outdated',
197+
message: `Method '${method}' is not supported by this LSP server.`,
198+
})
199+
}
200+
const response = await this.internal_call_custom_method(
201+
method,
202+
request as any,
203+
)
204+
if (isErr(response)) {
205+
return err({
206+
type: 'generic',
207+
message: response.error,
208+
})
209+
}
210+
return ok(response.value as Response)
211+
}
212+
case 'endpoint-not-supported': {
213+
const response = await this.internal_call_custom_method(
214+
method,
215+
request as any,
216+
)
217+
if (isErr(response)) {
218+
return err({
219+
type: 'generic',
220+
message: response.error,
221+
})
222+
}
223+
return ok(response.value as Response)
224+
}
225+
}
226+
}
227+
228+
/**
229+
* Internal method to call a custom LSP method without checking if the method is supported. It is used for
230+
* the class whereas the `call_custom_method` checks if the method is supported.
231+
*/
232+
public async internal_call_custom_method<
109233
Method extends CustomLSPMethods['method'],
110234
Request extends Extract<CustomLSPMethods, { method: Method }>['request'],
111235
Response extends Extract<CustomLSPMethods, { method: Method }>['response'],
112236
>(method: Method, request: Request): Promise<Result<Response, string>> {
113237
if (!this.client) {
114238
return err('lsp client not ready')
115239
}
240+
116241
try {
117242
const result = await this.client.sendRequest<Response>(method, request)
118243
return ok(result)

vscode/extension/src/utilities/errors.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,53 @@ import { traceInfo } from './common/log'
77
* Represents different types of errors that can occur in the application.
88
*/
99
export type ErrorType =
10-
| { type: 'generic'; message: string }
10+
| ErrorTypeGeneric
1111
| { type: 'not_signed_in' }
1212
| { type: 'sqlmesh_lsp_not_found' }
1313
// tcloud_bin_not_found is used when the tcloud executable is not found. This is likely to happen if the user
1414
// opens a project that has a `tcloud.yaml` file but doesn't have tcloud installed.
1515
| { type: 'tcloud_bin_not_found' }
1616
| SqlmeshLspDependenciesMissingError
17+
| ErrorTypeInvalidState
18+
| ErrorTypeSQLMeshOutdated
19+
20+
/**
21+
* ErrorTypeSQLMeshOutdated is used when the SQLMesh version is outdated. The
22+
* message should explain the problem, but the suggestion to update SQLMesh is
23+
* handled at the place where the error is shown.
24+
*/
25+
export interface ErrorTypeSQLMeshOutdated {
26+
type: 'sqlmesh_outdated'
27+
/**
28+
* A message that describes the outdated SQLMesh version, it should not talk about
29+
* updating SQLMesh. This is done at the place where the error is handled.
30+
*/
31+
message: string
32+
}
33+
34+
/**
35+
* ErrorTypeInvalidState is used when the state of the application is invalid state.
36+
* They should never be thrown by the application unless there is a bug in the code.
37+
* The shown message should be generic and not contain any sensitive information but
38+
* asks the user to report the issue to the developers.
39+
*/
40+
export interface ErrorTypeInvalidState {
41+
type: 'invalid_state'
42+
/**
43+
* A message that describes the invalid state, it should not talk about reporting
44+
* the issue to the developers. This is done at the place where the error is
45+
* handled.
46+
*/
47+
message: string
48+
}
49+
50+
/**
51+
* ErrorTypeGeneric is a generic error type that can be used to represent any error with a message.
52+
*/
53+
export interface ErrorTypeGeneric {
54+
type: 'generic'
55+
message: string
56+
}
1757

1858
/**
1959
* SqlmeshLspDependenciesMissingError is used when the sqlmesh_lsp is found but
@@ -33,6 +73,16 @@ export async function handleError(
3373
): Promise<void> {
3474
traceInfo('handleError', error)
3575
switch (error.type) {
76+
case 'invalid_state':
77+
await window.showErrorMessage(
78+
`Invalid state: ${error.message}. Please report this issue to the developers.`,
79+
)
80+
return
81+
case 'sqlmesh_outdated':
82+
await window.showErrorMessage(
83+
`SQLMesh itself is outdated. Please update SQLMesh to the latest version to use this feature. ${error.message}`,
84+
)
85+
return
3686
case 'not_signed_in':
3787
return handleNotSignedInError(authProvider)
3888
case 'sqlmesh_lsp_not_found':

0 commit comments

Comments
 (0)