diff --git a/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts b/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts index db13718be7..d5a0553da6 100644 --- a/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts +++ b/apps/desktop/src/lib/__tests__/sql/sqlStatementRanges.spec.ts @@ -1409,10 +1409,23 @@ FROM orders;`; expect(range?.sql).toBe("SELECT 1"); }); - it("skips MySQL delimiter commands when resolving the cursor statement", () => { + it("resolves MySQL delimiter command lines to the nearest executable statement", () => { const sql = "select COUNT(1) FROM your_table;\ndelimiter ;;\nselect COUNT(1) FROM your_table;\n\n;;\ndelimiter ;"; expect(statementRangeAtCursor(sql, indexOf(sql, "COUNT", 2), "mysql")?.sql.trim()).toBe("select COUNT(1) FROM your_table;"); - expect(statementRangeAtCursor(sql, indexOf(sql, "delimiter"), "mysql")).toBeNull(); + // A caret on a leading `delimiter ;;` line targets the statement it introduces. + expect(statementRangeAtCursor(sql, indexOf(sql, "delimiter"), "mysql")?.sql.trim()).toBe("select COUNT(1) FROM your_table;"); + // A trailing `delimiter ;` has nothing after it, so it targets the statement above. + expect(statementRangeAtCursor(sql, indexOf(sql, "delimiter", 2), "mysql")?.sql.trim()).toBe("select COUNT(1) FROM your_table;"); + }); + + it("targets the surrounding MySQL statements for delimiter lines in a routine script", () => { + const routine = mysqlDelimitedRoutineFixture.slice(mysqlDelimitedRoutineFixture.indexOf("CREATE PROCEDURE"), mysqlDelimitedRoutineFixture.indexOf(" //\nDELIMITER")); + expect(statementRangeAtCursor(mysqlDelimitedRoutineFixture, indexOf(mysqlDelimitedRoutineFixture, "DELIMITER"), "mysql")?.sql.trim()).toBe(routine); + expect(statementRangeAtCursor(mysqlDelimitedRoutineFixture, indexOf(mysqlDelimitedRoutineFixture, "DELIMITER", 2), "mysql")?.sql.trim()).toBe("CALL sp_insert_random_users(100)"); + }); + + it("returns null for a MySQL script made only of delimiter commands", () => { + expect(statementRangeAtCursor("delimiter //\n", 0, "mysql")).toBeNull(); }); it("returns the full MySQL routine block for cursors inside nested statements", () => { @@ -1855,6 +1868,13 @@ WHERE t2.product_name = '12345' expect(candidateSummaries(candidates)).toEqual(["cursor:select COUNT(1) FROM your_table;", "all:select COUNT(1) FROM your_table;\ndelimiter ;;\nselect COUNT(1) FROM your_table;\n\n;;\ndelimiter ;"]); }); + it("builds a cursor candidate for a MySQL delimiter command line (issue #9485)", () => { + const sql = "select COUNT(1) FROM your_table;\ndelimiter ;;\nselect COUNT(1) FROM your_table;\n\n;;\ndelimiter ;"; + const candidates = buildExecutionCandidates(sql, indexOf(sql, "delimiter"), "mysql"); + expect(candidateKinds(candidates)).toEqual(["cursor", "all"]); + expect(candidates[0].sql).toBe("select COUNT(1) FROM your_table;"); + }); + it("uses the current SQL Server batch for cursor candidates", () => { const sql = "SELECT 1\nGO\nSELECT 2;"; const candidates = buildExecutionCandidates(sql, indexOf(sql, "2"), "sqlserver"); diff --git a/apps/desktop/src/lib/sql/sqlStatementRanges.ts b/apps/desktop/src/lib/sql/sqlStatementRanges.ts index 1659d2c0d4..b25c3d43f3 100644 --- a/apps/desktop/src/lib/sql/sqlStatementRanges.ts +++ b/apps/desktop/src/lib/sql/sqlStatementRanges.ts @@ -709,6 +709,14 @@ export function statementRangeAtCursor(sql: string, cursorPos: number, databaseT } } + // A MySQL `delimiter X` line is a client directive, not executable SQL, so it + // never forms a statement range of its own. When the caret rests on such a + // line, target the statement the directive introduces (or the closest + // preceding statement when the directive ends the script) instead of + // reporting that there is nothing to run. See issue #9485. + const directiveRange = mysqlDelimiterDirectiveCursorRange(sql, pos, databaseType, parameterOptions, statements); + if (directiveRange) return directiveRange; + return null; } @@ -2192,6 +2200,29 @@ function parseDelimiterCommand(line: string): string | null { return delimiter ? delimiter : null; } +/** + * Resolves a caret resting on a MySQL `delimiter X` client-directive line to + * the nearest executable statement: the statement the directive introduces, or + * the statement just before it when the directive has nothing after it. + */ +function mysqlDelimiterDirectiveCursorRange(sql: string, pos: number, databaseType: DatabaseType | undefined, parameterOptions: SqlParameterOptions | undefined, statements: RawStatement[]): SqlTextRange | null { + if (databaseType !== "mysql" || statements.length === 0) return null; + const lineStart = sql.lastIndexOf("\n", pos - 1) + 1; + const lineEnd = findLineEnd(sql, pos); + let directiveStart = lineStart; + while (directiveStart < lineEnd && (sql[directiveStart] === " " || sql[directiveStart] === "\t")) directiveStart += 1; + if (!startsDelimiterCommand(sql, directiveStart)) return null; + if (parseDelimiterCommand(sql.slice(directiveStart, lineEnd)) === null) return null; + + const following = statements.find((statement) => statement.from >= lineEnd); + if (following) { + return rangeFor(splitStatementRangeAtSoftStarts(sql, following, databaseType, parameterOptions)[0] ?? following, sql); + } + const preceding = statements[statements.length - 1]; + const precedingSoftRanges = splitStatementRangeAtSoftStarts(sql, preceding, databaseType, parameterOptions); + return rangeFor(precedingSoftRanges[precedingSoftRanges.length - 1] ?? preceding, sql); +} + function findLineEnd(sql: string, pos: number): number { const newline = sql.indexOf("\n", pos); const carriageReturn = sql.indexOf("\r", pos); diff --git a/docs/screenshots/issue-9485-delimiter-after.png b/docs/screenshots/issue-9485-delimiter-after.png new file mode 100644 index 0000000000..31b073d7f0 Binary files /dev/null and b/docs/screenshots/issue-9485-delimiter-after.png differ diff --git a/docs/screenshots/issue-9485-delimiter-before.png b/docs/screenshots/issue-9485-delimiter-before.png new file mode 100644 index 0000000000..047c1c8fb8 Binary files /dev/null and b/docs/screenshots/issue-9485-delimiter-before.png differ diff --git a/docs/screenshots/issue-9485-delimiter-following.png b/docs/screenshots/issue-9485-delimiter-following.png new file mode 100644 index 0000000000..ca8a8b4365 Binary files /dev/null and b/docs/screenshots/issue-9485-delimiter-following.png differ