Skip to content

Commit 3ec3688

Browse files
committed
fix(vscode): ensure only load context once
1 parent 490b3ef commit 3ec3688

File tree

2 files changed

+73
-11
lines changed

2 files changed

+73
-11
lines changed

sqlmesh/lsp/main.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ class ContextFailed:
8585
"""State when context failed to load with an error message."""
8686

8787
error: ContextFailedError
88-
context: t.Optional[Context] = None
88+
context: Context
8989
version_id: str = field(default_factory=lambda: str(uuid.uuid4()))
9090

9191

@@ -227,24 +227,26 @@ def _reload_context_and_publish_diagnostics(
227227
self, ls: LanguageServer, uri: URI, document_uri: str
228228
) -> None:
229229
"""Helper method to reload context and publish diagnostics."""
230+
context: t.Optional[Context] = None
230231
if isinstance(self.context_state, NoContext):
231232
pass
232233
elif isinstance(self.context_state, ContextFailed):
233234
if self.context_state.context:
235+
context = self.context_state.context
234236
try:
235-
self.context_state.context.load()
237+
context.load()
236238
# Creating a new LSPContext will naturally create fresh caches
237-
self.context_state = ContextLoaded(
238-
lsp_context=LSPContext(self.context_state.context)
239-
)
239+
self.context_state = ContextLoaded(lsp_context=LSPContext(context))
240240
except Exception as e:
241241
ls.log_trace(f"Error loading context: {e}")
242-
context = (
242+
error_context = (
243243
self.context_state.context
244244
if hasattr(self.context_state, "context")
245245
else None
246246
)
247-
self.context_state = ContextFailed(error=e, context=context)
247+
if error_context is None:
248+
raise RuntimeError(f"Context should always be set.")
249+
self.context_state = ContextFailed(error=e, context=error_context)
248250
else:
249251
# If there's no context, reset to NoContext and try to create one from scratch
250252
ls.log_trace("No partial context available, attempting fresh creation")
@@ -860,17 +862,20 @@ def _create_lsp_context(self, paths: t.List[Path]) -> t.Optional[LSPContext]:
860862
Returns:
861863
A new LSPContext instance wrapping the created context, or None if creation fails
862864
"""
865+
context = None
863866
try:
864867
if isinstance(self.context_state, NoContext):
865-
context = self.context_class(paths=paths)
868+
context = self.context_class(paths=paths, load=False)
869+
context.load()
866870
loaded_sqlmesh_message(self.server, paths[0])
867871
elif isinstance(self.context_state, ContextFailed):
868872
if self.context_state.context:
869873
context = self.context_state.context
870874
context.load()
871875
else:
872876
# If there's no context (initial creation failed), try creating again
873-
context = self.context_class(paths=paths)
877+
context = self.context_class(paths=paths, load=False)
878+
context.load()
874879
loaded_sqlmesh_message(self.server, paths[0])
875880
else:
876881
context = self.context_state.lsp_context.context
@@ -889,11 +894,12 @@ def _create_lsp_context(self, paths: t.List[Path]) -> t.Optional[LSPContext]:
889894
self.server.log_trace(f"Error creating context: {e}")
890895
# Store the error in context state so subsequent requests show the actual error
891896
# Try to preserve any partially loaded context if it exists
892-
context = None
893897
if isinstance(self.context_state, ContextLoaded):
894898
context = self.context_state.lsp_context.context
895899
elif isinstance(self.context_state, ContextFailed) and self.context_state.context:
896900
context = self.context_state.context
901+
if context is None:
902+
raise RuntimeError("Context should always be set.")
897903
self.context_state = ContextFailed(error=e, context=context)
898904
return None
899905

vscode/extension/tests/broken_project.spec.ts

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,12 @@ import { test, expect } from './fixtures'
22
import fs from 'fs-extra'
33
import os from 'os'
44
import path from 'path'
5-
import { openLineageView, saveFile, SUSHI_SOURCE_PATH } from './utils'
5+
import {
6+
openLineageView,
7+
runCommand,
8+
saveFile,
9+
SUSHI_SOURCE_PATH,
10+
} from './utils'
611
import { createPythonInterpreterSettingsSpecifier } from './utils_code_server'
712

813
test('bad project, double model', async ({ page, sharedCodeServer }) => {
@@ -253,3 +258,54 @@ test('bad project, double model, check lineage', async ({
253258

254259
await page.waitForTimeout(500)
255260
})
261+
262+
test('bad model block, then fixed', async ({ page, sharedCodeServer }) => {
263+
const tempDir = await fs.mkdtemp(
264+
path.join(os.tmpdir(), 'vscode-test-tcloud-'),
265+
)
266+
// Copy over the sushi project
267+
await fs.copy(SUSHI_SOURCE_PATH, tempDir)
268+
await createPythonInterpreterSettingsSpecifier(tempDir)
269+
270+
// Add a model with a bad model block
271+
const badModelPath = path.join(tempDir, 'models', 'bad_model.sql')
272+
const contents =
273+
'MODEL ( name sushi.bad_block, test); SELECT * FROM sushi.customers'
274+
await fs.writeFile(badModelPath, contents)
275+
276+
await page.goto(
277+
`http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`,
278+
)
279+
await page.waitForLoadState('networkidle')
280+
281+
// Open the customers.sql model
282+
await page
283+
.getByRole('treeitem', { name: 'models', exact: true })
284+
.locator('a')
285+
.click()
286+
await page
287+
.getByRole('treeitem', { name: 'customers.sql', exact: true })
288+
.locator('a')
289+
.click()
290+
291+
// Wait for the error to appear
292+
await page.waitForSelector('text=Error creating context')
293+
294+
// Open the problems view
295+
await runCommand(page, 'View: Focus Problems')
296+
297+
// Assert error is present in the problems view
298+
const errorElement = page
299+
.getByText("Required keyword: 'value' missing for")
300+
.first()
301+
await expect(errorElement).toBeVisible({ timeout: 5000 })
302+
303+
// Remove the bad model file
304+
await fs.remove(badModelPath)
305+
306+
// Save to refresh the context
307+
await saveFile(page)
308+
309+
// Wait for successful context load
310+
await page.waitForSelector('text=Loaded SQLMesh context')
311+
})

0 commit comments

Comments
 (0)