From 1499008ba87339a4736e421ce3a7e9387f711fdc Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Wed, 1 Jul 2026 11:48:43 +0800 Subject: [PATCH] fix(editor): suppress SQL completion in strings --- .../src/components/editor/QueryEditor.vue | 13 +++++++++- apps/desktop/src/lib/sqlCompletion.ts | 26 +++++++++++++++++-- packages/app-tests/sqlCompletion.test.ts | 24 +++++++++++++++++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/components/editor/QueryEditor.vue b/apps/desktop/src/components/editor/QueryEditor.vue index f756dc0205..7ba6c95be0 100644 --- a/apps/desktop/src/components/editor/QueryEditor.vue +++ b/apps/desktop/src/components/editor/QueryEditor.vue @@ -18,7 +18,17 @@ import { useConnectionStore } from "@/stores/connectionStore"; import { useSettingsStore } from "@/stores/settingsStore"; import { useTheme } from "@/composables/useTheme"; import { useToast } from "@/composables/useToast"; -import { buildSqlCompletionItemsFromContext, getSqlFunctionSignatureHelp, getSqlCompletionContext, getSqlCompletionResultValidFor, isSqlLikeCompletionStatement, recordCompletionSelection, shouldAutoOpenSqlCompletion, extractCteDefinitions } from "@/lib/sqlCompletion"; +import { + buildSqlCompletionItemsFromContext, + getSqlFunctionSignatureHelp, + getSqlCompletionContext, + getSqlCompletionResultValidFor, + isSqlCompletionSuppressedContext, + isSqlLikeCompletionStatement, + recordCompletionSelection, + shouldAutoOpenSqlCompletion, + extractCteDefinitions, +} from "@/lib/sqlCompletion"; import { buildElasticsearchCompletionItemsFromContext, getElasticsearchCompletionContext, getElasticsearchCompletionResultValidFor, shouldAutoOpenElasticsearchCompletion, type ElasticsearchCompletionItem } from "@/lib/elasticsearchCompletion"; import { buildMongoCompletionItemsFromContext, getMongoCompletionContext, getMongoCompletionResultValidFor, shouldAutoOpenMongoCompletion, type MongoCompletionItem } from "@/lib/mongoCompletion"; import { extractIdentifierAt, isSqlKeyword, matchTable } from "@/lib/sqlNavigation"; @@ -1410,6 +1420,7 @@ async function provideSqlCompletions(currentState: import("@codemirror/state").E const epoch = ++completionEpoch; try { + if (isSqlCompletionSuppressedContext(fullDoc, position)) return null; if (!explicit && !shouldAutoOpenSqlCompletion(fullDoc, position)) return null; const completionContext = getSqlCompletionContext(fullDoc, position); diff --git a/apps/desktop/src/lib/sqlCompletion.ts b/apps/desktop/src/lib/sqlCompletion.ts index e7cb11cb73..08d36ce3ca 100644 --- a/apps/desktop/src/lib/sqlCompletion.ts +++ b/apps/desktop/src/lib/sqlCompletion.ts @@ -1185,6 +1185,7 @@ export function buildSqlCompletionItems( autoAliasTables?: boolean; }, ): SqlCompletionItem[] { + if (isSqlCompletionSuppressedContext(sql, cursor)) return []; const context = getSqlCompletionContext(sql, cursor); return buildSqlCompletionItemsFromContext(context, input); } @@ -1290,7 +1291,7 @@ class SqlCompletionProvider { } export function shouldAutoOpenSqlCompletion(sql: string, cursor: number): boolean { - if (isSqlCommentContext(sql, cursor)) return false; + if (isSqlCompletionSuppressedContext(sql, cursor)) return false; const previousChar = sql[cursor - 1]; if (!previousChar) return false; if (/\bon\s+$/i.test(sql.slice(0, cursor))) return true; @@ -1303,7 +1304,21 @@ export function shouldAutoOpenSqlCompletion(sql: string, cursor: number): boolea return /[\w$.@]/.test(previousChar); } +export function isSqlCompletionSuppressedContext(sql: string, cursor: number): boolean { + const context = getSqlLexicalContext(sql, cursor); + return context.inLineComment || context.inBlockComment || context.inStringLiteral; +} + +export function isSqlStringLiteralContext(sql: string, cursor: number): boolean { + return getSqlLexicalContext(sql, cursor).inStringLiteral; +} + export function isSqlCommentContext(sql: string, cursor: number): boolean { + const context = getSqlLexicalContext(sql, cursor); + return context.inLineComment || context.inBlockComment; +} + +function getSqlLexicalContext(sql: string, cursor: number): { inLineComment: boolean; inBlockComment: boolean; inStringLiteral: boolean } { const end = Math.max(0, Math.min(cursor, sql.length)); let inSingleQuote = false; let inDoubleQuote = false; @@ -1376,7 +1391,14 @@ export function isSqlCommentContext(sql: string, cursor: number): boolean { } } - return inLineComment || inBlockComment; + // Only single-quoted text is a value literal here. Double quotes, backticks, + // and brackets delimit identifiers in common SQL dialects, so they must not + // suppress identifier completion. + return { + inLineComment, + inBlockComment, + inStringLiteral: inSingleQuote, + }; } export function isSqlLikeCompletionStatement(sql: string, cursor: number): boolean { diff --git a/packages/app-tests/sqlCompletion.test.ts b/packages/app-tests/sqlCompletion.test.ts index 1cb55bae12..600fddf6a7 100644 --- a/packages/app-tests/sqlCompletion.test.ts +++ b/packages/app-tests/sqlCompletion.test.ts @@ -5,6 +5,7 @@ import { getSqlFunctionSignatureHelp, getSqlCompletionResultValidFor, isSqlCommentContext, + isSqlStringLiteralContext, shouldAutoOpenSqlCompletion, extractCteDefinitions, getSqlCompletionContext, @@ -798,6 +799,29 @@ test("does not auto-open completion inside SQL comments", () => { assert.equal(shouldAutoOpenSqlCompletion("select /* comment */ val", "select /* comment */ val".length), true); }); +test("does not auto-open or build metadata completion inside SQL string literals", () => { + const likeSql = "select * from orders where status like '%9250%'"; + const likeCursor = "select * from orders where status like '%9250%".length; + assert.equal(isSqlStringLiteralContext(likeSql, likeCursor), true); + assert.equal(shouldAutoOpenSqlCompletion(likeSql, likeCursor), false); + assert.deepEqual( + buildSqlCompletionItems(likeSql, likeCursor, { + tables, + columnsByTable, + }), + [], + ); + + const escapedQuoteSql = "select * from orders where status = 'it''s 9250'"; + const escapedQuoteCursor = "select * from orders where status = 'it''s 9250".length; + assert.equal(isSqlStringLiteralContext(escapedQuoteSql, escapedQuoteCursor), true); + assert.equal(shouldAutoOpenSqlCompletion(escapedQuoteSql, escapedQuoteCursor), false); + + const afterLiteralSql = "select '-- not comment' as value"; + assert.equal(isSqlStringLiteralContext(afterLiteralSql, afterLiteralSql.length), false); + assert.equal(shouldAutoOpenSqlCompletion(afterLiteralSql, afterLiteralSql.length), true); +}); + test("auto-opens completion after word characters and explicit dot qualifiers", () => { for (const sql of ["sel", "select * from us", "select u."]) { assert.equal(shouldAutoOpenSqlCompletion(sql, sql.length), true, sql);