Skip to content

Commit 2e5e2c8

Browse files
authored
feat: use new tcloud sqlmesh_lsp call (#4762)
1 parent ea35b3c commit 2e5e2c8

5 files changed

Lines changed: 336 additions & 3 deletions

File tree

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { describe, it, expect } from 'vitest'
2+
import { isSemVerGreaterThanOrEqual } from './semver'
3+
4+
describe('isSemVerGreaterThanOrEqual', () => {
5+
it('should return true when major version is greater', () => {
6+
expect(isSemVerGreaterThanOrEqual([2, 0, 0], [1, 0, 0])).toBe(true)
7+
expect(isSemVerGreaterThanOrEqual([3, 0, 0], [2, 5, 10])).toBe(true)
8+
})
9+
10+
it('should return false when major version is less', () => {
11+
expect(isSemVerGreaterThanOrEqual([1, 0, 0], [2, 0, 0])).toBe(false)
12+
expect(isSemVerGreaterThanOrEqual([0, 10, 20], [1, 0, 0])).toBe(false)
13+
})
14+
15+
it('should compare minor version when major versions are equal', () => {
16+
expect(isSemVerGreaterThanOrEqual([1, 2, 0], [1, 1, 0])).toBe(true)
17+
expect(isSemVerGreaterThanOrEqual([1, 1, 0], [1, 2, 0])).toBe(false)
18+
expect(isSemVerGreaterThanOrEqual([2, 5, 0], [2, 3, 10])).toBe(true)
19+
})
20+
21+
it('should compare patch version when major and minor versions are equal', () => {
22+
expect(isSemVerGreaterThanOrEqual([1, 1, 2], [1, 1, 1])).toBe(true)
23+
expect(isSemVerGreaterThanOrEqual([1, 1, 1], [1, 1, 2])).toBe(false)
24+
expect(isSemVerGreaterThanOrEqual([2, 3, 10], [2, 3, 5])).toBe(true)
25+
})
26+
27+
it('should return true when versions are equal', () => {
28+
expect(isSemVerGreaterThanOrEqual([1, 0, 0], [1, 0, 0])).toBe(true)
29+
expect(isSemVerGreaterThanOrEqual([2, 5, 10], [2, 5, 10])).toBe(true)
30+
})
31+
32+
it('should handle zero versions correctly', () => {
33+
expect(isSemVerGreaterThanOrEqual([0, 0, 1], [0, 0, 0])).toBe(true)
34+
expect(isSemVerGreaterThanOrEqual([0, 1, 0], [0, 0, 10])).toBe(true)
35+
expect(isSemVerGreaterThanOrEqual([0, 0, 0], [0, 0, 0])).toBe(true)
36+
})
37+
})
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
type SemVer = [number, number, number]
2+
3+
/**
4+
* Check if a is greater than or equal to b.
5+
*
6+
* @param a - The first version.
7+
* @param b - The second version.
8+
* @returns True if a is greater than b, false otherwise.
9+
*/
10+
export function isSemVerGreaterThanOrEqual(a: SemVer, b: SemVer): boolean {
11+
if (a[0] > b[0]) {
12+
return true
13+
}
14+
if (a[0] < b[0]) {
15+
return false
16+
}
17+
if (a[1] > b[1]) {
18+
return true
19+
}
20+
if (a[1] < b[1]) {
21+
return false
22+
}
23+
return a[2] >= b[2]
24+
}

vscode/extension/src/utilities/sqlmesh/sqlmesh.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import z from 'zod'
1212
import { ProgressLocation, window } from 'vscode'
1313
import { IS_WINDOWS } from '../isWindows'
1414
import { resolveProjectPath } from '../config'
15+
import { isSemVerGreaterThanOrEqual } from '../semver'
1516

1617
export interface SqlmeshExecInfo {
1718
workspacePath: string
@@ -394,6 +395,23 @@ export const sqlmeshLspExec = async (): Promise<
394395
if (isErr(ensured)) {
395396
return ensured
396397
}
398+
const tcloudBinVersion = await getTcloudBinVersion()
399+
if (isErr(tcloudBinVersion)) {
400+
return tcloudBinVersion
401+
}
402+
// TODO: Remove this once we have a stable version of tcloud that supports sqlmesh_lsp.
403+
if (isSemVerGreaterThanOrEqual(tcloudBinVersion.value, [2, 10, 1])) {
404+
return ok ({
405+
bin: tcloudBin.value,
406+
workspacePath,
407+
env: {
408+
PYTHONPATH: interpreterDetails.path?.[0],
409+
VIRTUAL_ENV: path.dirname(interpreterDetails.binPath!),
410+
PATH: interpreterDetails.binPath!,
411+
},
412+
args: ['sqlmesh_lsp'],
413+
})
414+
}
397415
}
398416
const binPath = path.join(interpreterDetails.binPath!, sqlmeshLSP)
399417
traceLog(`Bin path: ${binPath}`)
@@ -445,4 +463,31 @@ async function doesExecutableExist(executable: string): Promise<boolean> {
445463
traceLog(`Checked if ${executable} exists with ${command}, errored, returning false`)
446464
return false
447465
}
466+
}
467+
468+
/**
469+
* Get the version of the tcloud bin.
470+
*
471+
* @returns The version of the tcloud bin.
472+
*/
473+
async function getTcloudBinVersion(): Promise<Result<[number, number, number], ErrorType>> {
474+
const tcloudBin = await getTcloudBin()
475+
if (isErr(tcloudBin)) {
476+
return tcloudBin
477+
}
478+
const called = await execAsync(tcloudBin.value, ['--version'])
479+
if (called.exitCode !== 0) {
480+
return err({
481+
type: 'generic',
482+
message: `Failed to get tcloud bin version: ${called.stderr}`,
483+
})
484+
}
485+
const version = called.stdout.split('.').map(Number)
486+
if (version.length !== 3) {
487+
return err({
488+
type: 'generic',
489+
message: `Failed to get tcloud bin version: ${called.stdout}`,
490+
})
491+
}
492+
return ok(version as [number, number, number])
448493
}

vscode/extension/tests/tcloud.spec.ts

Lines changed: 174 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,17 @@ async function setupAuthenticatedState(tempDir: string): Promise<void> {
5454
await fs.writeJson(authStateFile, authState)
5555
}
5656

57+
/**
58+
* Helper function to set the tcloud version for testing
59+
*/
60+
async function setTcloudVersion(
61+
tempDir: string,
62+
version: string,
63+
): Promise<void> {
64+
const versionStateFile = path.join(tempDir, '.tcloud_version_state.json')
65+
await fs.writeJson(versionStateFile, { version })
66+
}
67+
5768
test.describe('Tcloud', () => {
5869
test('not signed in, shows sign in window', async ({}, testInfo) => {
5970
testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation
@@ -77,6 +88,9 @@ test.describe('Tcloud', () => {
7788
`url: ${tcloudConfig.url}\norg: ${tcloudConfig.org}\nproject: ${tcloudConfig.project}\n`,
7889
)
7990

91+
// Set tcloud version to 2.10.0
92+
await setTcloudVersion(tempDir, '2.10.0')
93+
8094
// Set up Python environment with mock tcloud and sqlmesh
8195
const pythonPath = await setupPythonEnvironment(pythonEnvDir)
8296

@@ -126,7 +140,7 @@ test.describe('Tcloud', () => {
126140
}
127141
})
128142

129-
test('signed in and not installed shows installation window and can see output', async ({}, testInfo) => {
143+
test('signed in and not installed shows installation window', async ({}, testInfo) => {
130144
testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation
131145
const tempDir = await fs.mkdtemp(
132146
path.join(os.tmpdir(), 'vscode-test-tcloud-'),
@@ -151,6 +165,9 @@ test.describe('Tcloud', () => {
151165
// Write mock ".tcloud_auth_state.json" file
152166
await setupAuthenticatedState(tempDir)
153167

168+
// Set tcloud version to 2.10.0
169+
await setTcloudVersion(tempDir, '2.10.0')
170+
154171
// Set up Python environment with mock tcloud and sqlmesh
155172
const pythonPath = await setupPythonEnvironment(pythonEnvDir)
156173

@@ -199,4 +216,160 @@ test.describe('Tcloud', () => {
199216
await fs.remove(tempDir)
200217
}
201218
})
219+
220+
test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in old version when ready', async ({}, testInfo) => {
221+
testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation
222+
const tempDir = await fs.mkdtemp(
223+
path.join(os.tmpdir(), 'vscode-test-tcloud-'),
224+
)
225+
const pythonEnvDir = path.join(tempDir, '.venv')
226+
227+
try {
228+
// Copy sushi project
229+
await fs.copy(SUSHI_SOURCE_PATH, tempDir)
230+
231+
// Create a tcloud.yaml to mark this as a tcloud project
232+
const tcloudConfig = {
233+
url: 'https://mock.tobikodata.com',
234+
org: 'test-org',
235+
project: 'test-project',
236+
}
237+
await fs.writeFile(
238+
path.join(tempDir, 'tcloud.yaml'),
239+
`url: ${tcloudConfig.url}\norg: ${tcloudConfig.org}\nproject: ${tcloudConfig.project}\n`,
240+
)
241+
242+
// Write mock ".tcloud_auth_state.json" file
243+
await setupAuthenticatedState(tempDir)
244+
245+
// Set tcloud version to 2.10.0
246+
await setTcloudVersion(tempDir, '2.10.0')
247+
248+
// Set up Python environment with mock tcloud and sqlmesh
249+
const pythonPath = await setupPythonEnvironment(pythonEnvDir)
250+
251+
// Mark sqlmesh as installed
252+
const binDir = path.dirname(pythonPath)
253+
const installStateFile = path.join(binDir, '.sqlmesh_installed')
254+
await fs.writeFile(installStateFile, '')
255+
256+
// Configure VS Code settings to use our Python environment
257+
const settings = {
258+
'python.defaultInterpreterPath': pythonPath,
259+
'sqlmesh.environmentPath': pythonEnvDir,
260+
}
261+
await fs.ensureDir(path.join(tempDir, '.vscode'))
262+
await fs.writeJson(
263+
path.join(tempDir, '.vscode', 'settings.json'),
264+
settings,
265+
{ spaces: 2 },
266+
)
267+
268+
// Start VS Code
269+
const { window, close } = await startVSCode(tempDir)
270+
271+
// Open a SQL file to trigger SQLMesh activation
272+
// Wait for the models folder to be visible
273+
await window.waitForSelector('text=models')
274+
275+
// Click on the models folder
276+
await window
277+
.getByRole('treeitem', { name: 'models', exact: true })
278+
.locator('a')
279+
.click()
280+
281+
// Open the top_waiters model
282+
await window
283+
.getByRole('treeitem', { name: 'customers.sql', exact: true })
284+
.locator('a')
285+
.click()
286+
287+
// Verify the context loads successfully
288+
await window.waitForSelector('text=Loaded SQLMesh context')
289+
290+
// Close VS Code
291+
await close()
292+
} finally {
293+
// Clean up
294+
await fs.remove(tempDir)
295+
}
296+
})
297+
298+
test('tcloud sqlmesh_lsp command starts the sqlmesh_lsp in new version when ready', async ({}, testInfo) => {
299+
testInfo.setTimeout(120_000) // 2 minutes for venv creation and package installation
300+
const tempDir = await fs.mkdtemp(
301+
path.join(os.tmpdir(), 'vscode-test-tcloud-'),
302+
)
303+
const pythonEnvDir = path.join(tempDir, '.venv')
304+
305+
try {
306+
// Copy sushi project
307+
await fs.copy(SUSHI_SOURCE_PATH, tempDir)
308+
309+
// Create a tcloud.yaml to mark this as a tcloud project
310+
const tcloudConfig = {
311+
url: 'https://mock.tobikodata.com',
312+
org: 'test-org',
313+
project: 'test-project',
314+
}
315+
await fs.writeFile(
316+
path.join(tempDir, 'tcloud.yaml'),
317+
`url: ${tcloudConfig.url}\norg: ${tcloudConfig.org}\nproject: ${tcloudConfig.project}\n`,
318+
)
319+
320+
// Write mock ".tcloud_auth_state.json" file
321+
await setupAuthenticatedState(tempDir)
322+
323+
// Set tcloud version to 2.10.0
324+
await setTcloudVersion(tempDir, '2.10.1')
325+
326+
// Set up Python environment with mock tcloud and sqlmesh
327+
const pythonPath = await setupPythonEnvironment(pythonEnvDir)
328+
329+
// Mark sqlmesh as installed
330+
const binDir = path.dirname(pythonPath)
331+
const installStateFile = path.join(binDir, '.sqlmesh_installed')
332+
await fs.writeFile(installStateFile, '')
333+
334+
// Configure VS Code settings to use our Python environment
335+
const settings = {
336+
'python.defaultInterpreterPath': pythonPath,
337+
'sqlmesh.environmentPath': pythonEnvDir,
338+
}
339+
await fs.ensureDir(path.join(tempDir, '.vscode'))
340+
await fs.writeJson(
341+
path.join(tempDir, '.vscode', 'settings.json'),
342+
settings,
343+
{ spaces: 2 },
344+
)
345+
346+
// Start VS Code
347+
const { window, close } = await startVSCode(tempDir)
348+
349+
// Open a SQL file to trigger SQLMesh activation
350+
// Wait for the models folder to be visible
351+
await window.waitForSelector('text=models')
352+
353+
// Click on the models folder
354+
await window
355+
.getByRole('treeitem', { name: 'models', exact: true })
356+
.locator('a')
357+
.click()
358+
359+
// Open the top_waiters model
360+
await window
361+
.getByRole('treeitem', { name: 'customers.sql', exact: true })
362+
.locator('a')
363+
.click()
364+
365+
// Verify the context loads successfully
366+
await window.waitForSelector('text=Loaded SQLMesh context')
367+
368+
// Close VS Code
369+
await close()
370+
} finally {
371+
// Clean up
372+
await fs.remove(tempDir)
373+
}
374+
})
202375
})

0 commit comments

Comments
 (0)