From 5375bd14737b4a8f76192e54a0edf1f712c45c58 Mon Sep 17 00:00:00 2001 From: t8y2 <1156263951@qq.com> Date: Wed, 12 Aug 2026 20:35:26 +0000 Subject: [PATCH] fix(editor): run hinted SQL from statement gutter Closes #5986 --- .../sql/executableStatementRangeCache.spec.ts | 33 ++++++++++++++ .../lib/sql/executableStatementRangeCache.ts | 43 ++++++++++++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/lib/__tests__/sql/executableStatementRangeCache.spec.ts b/apps/desktop/src/lib/__tests__/sql/executableStatementRangeCache.spec.ts index 0ea6b1458e..633745a332 100644 --- a/apps/desktop/src/lib/__tests__/sql/executableStatementRangeCache.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/executableStatementRangeCache.spec.ts @@ -67,6 +67,39 @@ describe("executableStatementRangeCacheForDoc", () => { expect(executableStatementRangeStartingAt(cache, doc.line(5).from)).toBeNull(); }); + it.each(["/*& tenant:'gdx' */", "/*&tenant:mctest*/", "/*+ MAX_EXECUTION_TIME(1000) */", "/*@global:true*/", "/*proxy*/"])("resolves the SQL line after a preserved leading directive for gutter execution: %s", (directive) => { + const sql = `SELECT 0;\n${directive}\nSELECT 1;`; + const doc = Text.of(sql.split("\n")); + const cache = executableStatementRangeCacheForDoc(null, doc, "mysql"); + + expect(executableStatementRangeStartingAt(cache, doc.line(3).from)?.sql).toBe(`${directive}\nSELECT 1`); + }); + + it("keeps a same-line tenant hint in the gutter execution range", () => { + const sql = "/*& tenant:'gdx' */ SELECT\n*\nFROM table"; + const doc = Text.of(sql.split("\n")); + const cache = executableStatementRangeCacheForDoc(null, doc, "mysql"); + + expect(executableStatementRangeStartingAt(cache, doc.line(1).from)?.sql).toBe(sql); + }); + + it("treats a SQL Server temporary table after a hint as executable content", () => { + const sql = "/*+ hint */\n#temporary_table"; + const doc = Text.of(sql.split("\n")); + const cache = executableStatementRangeCacheForDoc(null, doc, "sqlserver"); + + expect(executableStatementRangeStartingAt(cache, doc.line(2).from)?.sql).toBe(sql); + }); + + it("does not attach ordinary leading block comments to gutter execution", () => { + const doc = Text.of(["/* comment */", "SELECT 1;", "/* comment only */"]); + const cache = executableStatementRangeCacheForDoc(null, doc, "mysql"); + + expect(executableStatementRangeStartingAt(cache, doc.line(1).from)).toBeNull(); + expect(executableStatementRangeStartingAt(cache, doc.line(2).from)?.sql).toBe("SELECT 1"); + expect(executableStatementRangeStartingAt(cache, doc.line(3).from)).toBeNull(); + }); + it("does not resolve gutter run buttons when non-whitespace precedes the statement on the same line", () => { const doc = Text.of(["/* comment */ SELECT 1;"]); const cache = executableStatementRangeCacheForDoc(null, doc, "mysql"); diff --git a/apps/desktop/src/lib/sql/executableStatementRangeCache.ts b/apps/desktop/src/lib/sql/executableStatementRangeCache.ts index f8d99b1ef5..5bb479cfaa 100644 --- a/apps/desktop/src/lib/sql/executableStatementRangeCache.ts +++ b/apps/desktop/src/lib/sql/executableStatementRangeCache.ts @@ -30,17 +30,58 @@ export function executableStatementRangeCacheForDoc( const byStart = new Map(); const byExecutableLineStart = new Map(); - const ranges = parse(doc.toString(), databaseType, parameterOptions); + const sql = doc.toString(); + const ranges = parse(sql, databaseType, parameterOptions); for (const range of ranges) { byStart.set(range.from, range); const line = doc.lineAt(range.from); if (doc.sliceString(line.from, range.from).trim() === "") { byExecutableLineStart.set(line.from, range); } + const executableStart = executableStartAfterLeadingDirective(sql, range, databaseType, parameterOptions); + if (executableStart !== null) { + byExecutableLineStart.set(doc.lineAt(executableStart).from, range); + } } return { doc, databaseType, parameterOptions, parameterSyntaxKey, byStart, byExecutableLineStart, ranges }; } +function executableStartAfterLeadingDirective(sql: string, range: SqlTextRange, databaseType?: DatabaseType, parameterOptions?: SqlParameterOptions): number | null { + const text = range.sql; + const hasExecutableDirective = text.startsWith("/*+") || text.startsWith("/*@") || text.startsWith("/*&") || (databaseType === "mysql" && text.startsWith("/*proxy*/")); + if (!hasExecutableDirective) return null; + + let offset = 0; + while (offset < text.length) { + while (/\s/.test(text[offset] ?? "")) offset += 1; + + if (text.startsWith("/*", offset)) { + const close = text.indexOf("*/", offset + 2); + if (close < 0) return null; + offset = close + 2; + continue; + } + + if (text.startsWith("--", offset)) { + const newline = text.indexOf("\n", offset + 2); + if (newline < 0) return null; + offset = newline + 1; + continue; + } + + if (databaseType !== "sqlserver" && text[offset] === "#" && readSqlBracedParameterAt(sql, range.from + offset, parameterOptions)?.syntax !== "mybatis") { + const newline = text.indexOf("\n", offset + 1); + if (newline < 0) return null; + offset = newline + 1; + continue; + } + + return offset < text.length ? range.from + offset : null; + } + + return null; +} + export function executableStatementRangeStartingAt(cache: ExecutableStatementRangeCache, lineFrom: number): SqlTextRange | null { return cache.byStart.get(lineFrom) ?? cache.byExecutableLineStart.get(lineFrom) ?? null; }