diff --git a/apps/desktop/src/lib/__tests__/sqlStatementRanges.spec.ts b/apps/desktop/src/lib/__tests__/sqlStatementRanges.spec.ts index 725339a0a..b94393abe 100644 --- a/apps/desktop/src/lib/__tests__/sqlStatementRanges.spec.ts +++ b/apps/desktop/src/lib/__tests__/sqlStatementRanges.spec.ts @@ -183,6 +183,26 @@ describe("statementRangeAtCursor", () => { expect(range?.sql.trim()).toBe("EXPLAIN\nSELECT * FROM users"); }); + it("keeps MySQL DESC UPDATE joins as one statement", () => { + const sql = "desc update test_orders a\njoin test_users b\non a.id=b.id \nset a.name = '张三'\nwhere b.id > 10;"; + expect(statementRangeAtCursor(sql, indexOf(sql, "desc"), "mysql")?.sql.trim()).toBe(sql.slice(0, -1)); + expect(statementRangeAtCursor(sql, indexOf(sql, "set"), "mysql")?.sql.trim()).toBe(sql.slice(0, -1)); + expect(rangeSqlTexts(executableStatementRanges(sql, "mysql"))).toEqual([sql.slice(0, -1)]); + }); + + it("keeps MySQL EXPLAIN UPDATE assignments as one statement", () => { + const sql = "EXPLAIN UPDATE test_orders a\nJOIN test_users b ON a.id=b.id\nSET a.name = '张三'\nWHERE b.id > 10;"; + expect(statementRangeAtCursor(sql, indexOf(sql, "SET"), "mysql")?.sql.trim()).toBe(sql.slice(0, -1)); + expect(rangeSqlTexts(executableStatementRanges(sql, "mysql"))).toEqual([sql.slice(0, -1)]); + }); + + it("does not merge a plain MySQL DESC table statement with the next query", () => { + const sql = "DESC users\nSELECT * FROM users;"; + expect(statementRangeAtCursor(sql, indexOf(sql, "DESC"), "mysql")?.sql.trim()).toBe("DESC users"); + expect(statementRangeAtCursor(sql, indexOf(sql, "SELECT"), "mysql")?.sql.trim()).toBe("SELECT * FROM users"); + expect(rangeSqlTexts(executableStatementRanges(sql, "mysql"))).toEqual(["DESC users", "SELECT * FROM users"]); + }); + it("does not include comments between soft statement blocks", () => { const sql = "SELECT 1\n-- explain the next query\n/* still next query notes */\nSELECT 2;"; const range = statementRangeAtCursor(sql, indexOf(sql, "1")); @@ -355,6 +375,11 @@ describe("hasMultipleExecutionTargets", () => { const sql = "select COUNT(1) FROM your_table;\ndelimiter ;;\nselect COUNT(1) FROM your_table;\n\n;;\ndelimiter ;"; expect(hasMultipleExecutionTargets(sql, "mysql")).toBe(true); }); + + it("does not show multiple targets for MySQL DESC UPDATE joins", () => { + const sql = "desc update test_orders a\njoin test_users b\non a.id=b.id \nset a.name = '张三'\nwhere b.id > 10;"; + expect(hasMultipleExecutionTargets(sql, "mysql")).toBe(false); + }); }); describe("supportsExecutionTargetPicker", () => { diff --git a/apps/desktop/src/lib/sqlStatementRanges.ts b/apps/desktop/src/lib/sqlStatementRanges.ts index a47d8f415..00771daf8 100644 --- a/apps/desktop/src/lib/sqlStatementRanges.ts +++ b/apps/desktop/src/lib/sqlStatementRanges.ts @@ -388,6 +388,8 @@ function splitStatementRangeAtSoftStarts(sql: string, statement: RawStatement, d const boundaries: Array<{ hitFrom: number; from: number; keyword: string }> = []; let currentKeyword = softStatementKeywordAt(sql, statement.from, databaseType); + let currentExplainTargetKeyword = explainLikeTargetKeywordAt(sql, statement.from); + let currentBodyKeyword = currentExplainTargetKeyword ?? currentKeyword; let consumedWithMainStatement = false; let consumedExplainStatement = false; @@ -401,25 +403,28 @@ function splitStatementRangeAtSoftStarts(sql: string, statement: RawStatement, d continue; } - if (currentKeyword === "EXPLAIN" && !consumedExplainStatement && EXPLAIN_STATEMENT_KEYWORDS.has(lineStart.keyword)) { + if (!consumedExplainStatement && EXPLAIN_STATEMENT_KEYWORDS.has(lineStart.keyword) && (currentKeyword === "EXPLAIN" || currentExplainTargetKeyword !== null)) { consumedExplainStatement = true; + currentBodyKeyword = lineStart.keyword; continue; } - if (currentKeyword === "CREATE" && CREATE_BODY_KEYWORDS.has(lineStart.keyword)) { + if (currentBodyKeyword === "CREATE" && CREATE_BODY_KEYWORDS.has(lineStart.keyword)) { continue; } - if (currentKeyword === "INSERT" && INSERT_BODY_KEYWORDS.has(lineStart.keyword)) { + if (currentBodyKeyword === "INSERT" && INSERT_BODY_KEYWORDS.has(lineStart.keyword)) { continue; } - if (currentKeyword === "UPDATE" && lineStart.keyword === "SET") { + if (currentBodyKeyword === "UPDATE" && lineStart.keyword === "SET") { continue; } boundaries.push(lineStart); currentKeyword = lineStart.keyword; + currentExplainTargetKeyword = explainLikeTargetKeywordAt(sql, lineStart.from); + currentBodyKeyword = currentExplainTargetKeyword ?? currentKeyword; consumedWithMainStatement = false; consumedExplainStatement = false; } @@ -618,6 +623,22 @@ function softStatementStartKeywords(databaseType?: DatabaseType): Set { return new Set([...COMMON_SOFT_STATEMENT_START_KEYWORDS, ...(databaseType ? (DATABASE_SOFT_STATEMENT_KEYWORDS[databaseType] ?? []) : [])]); } +function isExplainLikeKeyword(keyword: string | null): boolean { + return keyword === "EXPLAIN" || keyword === "DESCRIBE" || keyword === "DESC"; +} + +function explainLikeTargetKeywordAt(sql: string, pos: number): string | null { + const prefixMatch = /^[A-Za-z_][\w$]*/.exec(sql.slice(pos)); + const prefix = prefixMatch?.[0]?.toUpperCase(); + if (!isExplainLikeKeyword(prefix ?? null)) return null; + + let i = pos + (prefixMatch?.[0].length ?? 0); + while (i < sql.length && isSqlWhitespace(sql[i])) i += 1; + const targetMatch = /^[A-Za-z_][\w$]*/.exec(sql.slice(i)); + const targetKeyword = targetMatch?.[0]?.toUpperCase(); + return targetKeyword && EXPLAIN_STATEMENT_KEYWORDS.has(targetKeyword) ? targetKeyword : null; +} + function startsLineComment(sql: string, pos: number): boolean { return (sql[pos] === "-" && sql[pos + 1] === "-") || sql[pos] === "#"; }