diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4ab18b8d6a..d18077d1d3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -84,6 +84,9 @@ importers: vitest: specifier: ^3.2.4 version: 3.2.4(@types/debug@4.1.12)(@types/node@20.11.25)(@vitest/ui@3.2.4)(jiti@2.4.2)(jsdom@26.1.0)(lightningcss@1.30.1)(terser@5.43.1)(tsx@4.20.3)(yaml@2.8.0) + yaml: + specifier: ^2.8.0 + version: 2.8.0 vscode/react: dependencies: diff --git a/sqlmesh/core/config/loader.py b/sqlmesh/core/config/loader.py index f40646131d..1618d29ad2 100644 --- a/sqlmesh/core/config/loader.py +++ b/sqlmesh/core/config/loader.py @@ -91,6 +91,7 @@ def load_config_from_paths( "SQLMesh project config could not be found. Point the cli to the project path with `sqlmesh -p`. If you haven't set up the SQLMesh project, run `sqlmesh init`." ) + yaml_config_path: t.Optional[Path] = None for path in [*project_paths, *personal_paths]: if not path.exists(): continue @@ -107,8 +108,9 @@ def load_config_from_paths( if extension in ("yml", "yaml"): if config_name != "config" and not python_config: raise ConfigError( - "YAML configs do not support multiple configs. Use Python instead." + "YAML configs do not support multiple configs. Use Python instead.", ) + yaml_config_path = path.resolve() non_python_configs.append(load_config_from_yaml(path)) elif extension == "py": try: @@ -149,7 +151,8 @@ def load_config_from_paths( except ValidationError as e: raise ConfigError( validation_error_message(e, "Invalid project config:") - + "\n\nVerify your config.yaml and environment variables." + + "\n\nVerify your config.yaml and environment variables.", + location=yaml_config_path, ) no_dialect_err_msg = "Default model SQL dialect is a required configuration parameter. Set it in the `model_defaults` `dialect` key in your config file." diff --git a/sqlmesh/lsp/main.py b/sqlmesh/lsp/main.py index 771ebd19c1..c257ccfaa1 100755 --- a/sqlmesh/lsp/main.py +++ b/sqlmesh/lsp/main.py @@ -767,7 +767,7 @@ def completion( get_sql_completions(None, URI(params.text_document.uri)) return None - def _get_diagnostics_for_uri(self, uri: URI) -> t.Tuple[t.List[types.Diagnostic], int]: + def _get_diagnostics_for_uri(self, uri: URI) -> t.Tuple[t.List[types.Diagnostic], str]: """Get diagnostics for a specific URI, returning (diagnostics, result_id). Since we no longer track version numbers, we always return 0 as the result_id. @@ -776,12 +776,16 @@ def _get_diagnostics_for_uri(self, uri: URI) -> t.Tuple[t.List[types.Diagnostic] try: context = self._context_get_or_load(uri) diagnostics = context.lint_model(uri) - return LSPContext.diagnostics_to_lsp_diagnostics(diagnostics), 0 + return LSPContext.diagnostics_to_lsp_diagnostics( + diagnostics + ), self.context_state.version_id except ConfigError as config_error: diagnostic, error = context_error_to_diagnostic(config_error, uri_filter=uri) if diagnostic: - return [diagnostic[1]], 0 - return [], 0 + location, diag = diagnostic + if location == uri.value: + return [diag], self.context_state.version_id + return [], self.context_state.version_id def _context_get_or_load(self, document_uri: t.Optional[URI] = None) -> LSPContext: state = self.context_state diff --git a/vscode/extension/package.json b/vscode/extension/package.json index 4aa397cef5..9628ef662e 100644 --- a/vscode/extension/package.json +++ b/vscode/extension/package.json @@ -155,6 +155,7 @@ "ts-loader": "^9.5.2", "typescript": "^5.8.3", "typescript-eslint": "^8.35.1", - "vitest": "^3.2.4" + "vitest": "^3.2.4", + "yaml": "^2.8.0" } } diff --git a/vscode/extension/tests/broken_project.spec.ts b/vscode/extension/tests/broken_project.spec.ts index bc6f4f7ed1..744197e3bb 100644 --- a/vscode/extension/tests/broken_project.spec.ts +++ b/vscode/extension/tests/broken_project.spec.ts @@ -2,8 +2,15 @@ import { test, expect } from './fixtures' import fs from 'fs-extra' import os from 'os' import path from 'path' -import { openLineageView, saveFile, SUSHI_SOURCE_PATH } from './utils' +import { + openLineageView, + runCommand, + saveFile, + SUSHI_SOURCE_PATH, +} from './utils' import { createPythonInterpreterSettingsSpecifier } from './utils_code_server' +import { execAsync } from '../src/utilities/exec' +import yaml from 'yaml' test('bad project, double model', async ({ page, sharedCodeServer }) => { const tempDir = await fs.mkdtemp( @@ -253,3 +260,60 @@ test('bad project, double model, check lineage', async ({ await page.waitForTimeout(500) }) + +const setup = async (tempDir: string) => { + // Run the sqlmesh CLI from the root of the repo using the local path + const sqlmeshCliPath = path.resolve(__dirname, '../../../.venv/bin/sqlmesh') + const result = await execAsync(sqlmeshCliPath, ['init', 'duckdb'], { + cwd: tempDir, + }) + expect(result.exitCode).toBe(0) +} + +test.describe('Bad config.py/config.yaml file issues', () => { + test('sqlmesh init, then corrupted config.yaml, bad parameters', async ({ + page, + sharedCodeServer, + }) => { + const tempDir = await fs.mkdtemp( + path.join(os.tmpdir(), 'vscode-test-tcloud-'), + ) + await setup(tempDir) + await createPythonInterpreterSettingsSpecifier(tempDir) + + const configYamlPath = path.join(tempDir, 'config.yaml') + // Write an invalid YAML to config.yaml + const config = { + gateway: 'test', + } + // Write config to the yaml file + await fs.writeFile(configYamlPath, yaml.stringify(config)) + + await page.goto( + `http://127.0.0.1:${sharedCodeServer.codeServerPort}/?folder=${tempDir}`, + ) + await page.waitForLoadState('networkidle') + + // Open full_model.sql model + await page + .getByRole('treeitem', { name: 'models', exact: true }) + .locator('a') + .click() + await page + .getByRole('treeitem', { name: 'full_model.sql', exact: true }) + .locator('a') + .click() + + // Wait for the error to appear + await page.waitForSelector('text=Error creating context') + + // Open the problems view + await runCommand(page, 'View: Focus Problems') + + // Asser that the error is present in the problems view + await page + .getByText('Invalid project config:', { exact: true }) + .first() + .isVisible({ timeout: 5_000 }) + }) +})