diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index 1e4c177c72..1ae4fdea7d 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -243,6 +243,7 @@ let vimModeComp: import("@codemirror/state").Compartment | null = null; let closeBracketsComp: import("@codemirror/state").Compartment | null = null; let sqlLanguageComp: import("@codemirror/state").Compartment | null = null; let sqlSemanticHighlightComp: import("@codemirror/state").Compartment | null = null; +let sqlSignatureComp: import("@codemirror/state").Compartment | null = null; let codeMirrorCloseBrackets: typeof import("@codemirror/autocomplete").closeBrackets | null = null; let codeMirrorCloseBracketsKeymap: readonly import("@codemirror/view").KeyBinding[] | null = null; let readOnlyComp: import("@codemirror/state").Compartment | null = null; @@ -2910,6 +2911,7 @@ onMounted(async () => { closeBracketsComp = new Compartment(); sqlLanguageComp = new Compartment(); sqlSemanticHighlightComp = new Compartment(); + sqlSignatureComp = new Compartment(); codeMirrorCloseBrackets = closeBrackets; codeMirrorCloseBracketsKeymap = closeBracketsKeymap; readOnlyComp = new Compartment(); @@ -3130,7 +3132,7 @@ onMounted(async () => { buildSqlSignatureExtension = () => showTooltip.compute(["doc", "selection"], (currentState) => { - const signature = getSqlFunctionSignatureHelp(currentState.doc.toString(), currentState.selection.main.head); + const signature = getSqlFunctionSignatureHelp(currentState.doc.toString(), currentState.selection.main.head, props.databaseType); if (!signature) return null; return { pos: currentState.selection.main.head, @@ -3385,7 +3387,7 @@ onMounted(async () => { }), ), hoverTooltip((currentView, pos) => resolveSqlHoverTooltip(currentView, pos)), - buildSqlSignatureExtension(), + sqlSignatureComp.of(buildSqlSignatureExtension()), diagnosticComp.of(buildSqlDiagnosticExtension()), createInsertValueHintsExtension({ isEnabled: () => settingsStore.editorSettings.showInsertValueHints && props.databaseType !== "redis" && props.databaseType !== "mongodb" && props.databaseType !== "elasticsearch", @@ -3738,9 +3740,10 @@ watch( watch([() => props.databaseType, () => props.dialect, () => props.syntaxDialect], () => { executableStatementRangeCache = null; - if (!view.value || !sqlLanguageComp || !buildSqlLanguageExtension || !sqlSemanticHighlightComp || !buildSqlSemanticHighlightExtension) return; + if (!view.value || !sqlLanguageComp || !buildSqlLanguageExtension || !sqlSemanticHighlightComp || !buildSqlSemanticHighlightExtension || !sqlSignatureComp || !buildSqlSignatureExtension) return; + // Signature tooltips depend on the external dialect, so refresh them even when the document and selection stay unchanged. view.value.dispatch({ - effects: [sqlLanguageComp.reconfigure(buildSqlLanguageExtension()), sqlSemanticHighlightComp.reconfigure(buildSqlSemanticHighlightExtension())], + effects: [sqlLanguageComp.reconfigure(buildSqlLanguageExtension()), sqlSemanticHighlightComp.reconfigure(buildSqlSemanticHighlightExtension()), sqlSignatureComp.reconfigure(buildSqlSignatureExtension())], }); }); diff --git a/apps/desktop/src/lib/__tests__/editor/queryEditorSqlSignature.spec.ts b/apps/desktop/src/lib/__tests__/editor/queryEditorSqlSignature.spec.ts new file mode 100644 index 0000000000..38640fef5e --- /dev/null +++ b/apps/desktop/src/lib/__tests__/editor/queryEditorSqlSignature.spec.ts @@ -0,0 +1,19 @@ +import { readFileSync } from "node:fs"; +import { describe, expect, it } from "vitest"; + +const queryEditorSource = readFileSync(new URL("../../../components/editor/QueryEditor.vue", import.meta.url), "utf8"); + +describe("QueryEditor SQL signature refresh wiring", () => { + it("reconfigures signature help when the database dialect changes", () => { + expect(queryEditorSource).toContain('let sqlSignatureComp: import("@codemirror/state").Compartment | null = null;'); + expect(queryEditorSource).toContain("sqlSignatureComp.of(buildSqlSignatureExtension())"); + + const watcherStart = queryEditorSource.indexOf("watch([() => props.databaseType, () => props.dialect, () => props.syntaxDialect]"); + const watcherEnd = queryEditorSource.indexOf("\n});", watcherStart); + const dialectWatcher = queryEditorSource.slice(watcherStart, watcherEnd); + + expect(watcherStart).toBeGreaterThanOrEqual(0); + expect(watcherEnd).toBeGreaterThan(watcherStart); + expect(dialectWatcher).toContain("sqlSignatureComp.reconfigure(buildSqlSignatureExtension())"); + }); +}); diff --git a/apps/desktop/src/lib/sql/sqlCompletion.ts b/apps/desktop/src/lib/sql/sqlCompletion.ts index b8f3200612..f74c0fc0f5 100644 --- a/apps/desktop/src/lib/sql/sqlCompletion.ts +++ b/apps/desktop/src/lib/sql/sqlCompletion.ts @@ -954,6 +954,7 @@ const POSTGRES_FUNCTION_SIGNATURES = new Map([ ]); const MYSQL_FUNCTION_SIGNATURES = new Map([ + ["CONVERT", ["expression", "type"]], ["DATE_FORMAT", ["date", "format"]], ["FROM_UNIXTIME", ["unix_timestamp"]], ["UNIX_TIMESTAMP", []], @@ -975,6 +976,7 @@ const SQLITE_FUNCTION_SIGNATURES = new Map([ const CLOUDFLARE_D1_FUNCTION_SIGNATURES = new Map(Array.from(SQLITE_FUNCTION_SIGNATURES.entries()).filter(([name]) => name !== "NOW")); const SQLSERVER_FUNCTION_SIGNATURES = new Map([ + ["CONVERT", ["type", "expression"]], ["TRY_CAST", ["expression AS type"]], ["TRY_CONVERT", ["type", "expression"]], ["JSON_VALUE", ["expression", "path"]], @@ -1619,7 +1621,7 @@ export function getSqlCompletionResultValidFor(sql: string, cursor: number): Reg return undefined; } -export function getSqlFunctionSignatureHelp(sql: string, cursor: number): SqlFunctionSignatureHelp | null { +export function getSqlFunctionSignatureHelp(sql: string, cursor: number, databaseType?: DatabaseType): SqlFunctionSignatureHelp | null { const beforeCursor = sql.slice(0, cursor); const openParenIndex = findActiveFunctionOpenParen(beforeCursor); if (openParenIndex == null) return null; @@ -1628,7 +1630,7 @@ export function getSqlFunctionSignatureHelp(sql: string, cursor: number): SqlFun const name = /([A-Za-z_][\w$]*)$/.exec(beforeParen)?.[1]?.toUpperCase(); if (!name) return null; - const parameters = SQL_FUNCTION_SIGNATURES.get(name); + const parameters = (databaseType ? DATABASE_FUNCTION_SIGNATURES[databaseType]?.get(name) : undefined) ?? SQL_FUNCTION_SIGNATURES.get(name); if (!parameters) return null; const activeParameter = countTopLevelCommas(beforeCursor.slice(openParenIndex + 1)); diff --git a/packages/app-tests/sqlCompletion.test.ts b/packages/app-tests/sqlCompletion.test.ts index 356109b0b3..2062116aa2 100644 --- a/packages/app-tests/sqlCompletion.test.ts +++ b/packages/app-tests/sqlCompletion.test.ts @@ -1782,6 +1782,27 @@ test("returns cast signature with AS syntax", () => { }); }); +test("uses dialect-specific argument order for CONVERT completion and signature help", () => { + const sql = "select conv"; + const sqlServerItems = buildSqlCompletionItems(sql, sql.length, { + tables: [], + columnsByTable: new Map(), + databaseType: "sqlserver", + }); + const mysqlItems = buildSqlCompletionItems(sql, sql.length, { + tables: [], + columnsByTable: new Map(), + databaseType: "mysql", + }); + + assert.equal(sqlServerItems.find((item) => item.label === "CONVERT")?.apply, "CONVERT(${type}, ${expression})"); + assert.equal(mysqlItems.find((item) => item.label === "CONVERT")?.apply, "CONVERT(${expression}, ${type})"); + + const functionSql = "select convert("; + assert.deepEqual(getSqlFunctionSignatureHelp(functionSql, functionSql.length, "sqlserver")?.parameters, ["type", "expression"]); + assert.deepEqual(getSqlFunctionSignatureHelp(functionSql, functionSql.length, "mysql")?.parameters, ["expression", "type"]); +}); + test("returns null signature help outside function calls", () => { assert.equal(getSqlFunctionSignatureHelp("select created_at from users", "select created_at".length), null); });