fix(mysql): execute adjacent statement on delimiter directive lines

This commit is contained in:
zipg
2026-09-18 22:48:04 +08:00
committed by GitHub
parent 6d8e0d886a
commit 4699295ed6
5 changed files with 53 additions and 2 deletions
@@ -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");
@@ -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);
Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB