fix(sql): preserve ranges after incomplete EXPLAIN options
This commit is contained in:
1 parent
f9bf039aef
commit
d3118256a4
2 files changed
+191
-2
No files matched your search
@@ -487,8 +487,12 @@ function splitStatementRangeAtSoftStarts(sql: string, statement: RawStatement, d
|
||||
for (const lineStart of lineStarts) {
|
||||
if (lineStart.from <= statement.from) continue;
|
||||
|
||||
if (currentKeyword === "WITH" && !consumedWithMainStatement && WITH_MAIN_STATEMENT_KEYWORDS.has(lineStart.keyword)) {
|
||||
if (currentBodyKeyword === "WITH" && !consumedWithMainStatement && WITH_MAIN_STATEMENT_KEYWORDS.has(lineStart.keyword)) {
|
||||
consumedWithMainStatement = true;
|
||||
// The CTE main statement also satisfies a pending EXPLAIN target, and its
|
||||
// own body rules (e.g. UPDATE ... SET) must take over from here.
|
||||
consumedExplainStatement = true;
|
||||
currentBodyKeyword = lineStart.keyword;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -511,6 +515,10 @@ function splitStatementRangeAtSoftStarts(sql: string, statement: RawStatement, d
|
||||
}
|
||||
|
||||
if (currentBodyKeyword === "INSERT" && INSERT_BODY_KEYWORDS.has(lineStart.keyword)) {
|
||||
// Hand over to the source query's own rules so only its continuations
|
||||
// (CTE main statement, set operations) stay attached to the INSERT.
|
||||
currentBodyKeyword = lineStart.keyword;
|
||||
if (lineStart.keyword === "WITH") consumedWithMainStatement = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -553,6 +561,10 @@ function splitStatementRangeAtSoftStarts(sql: string, statement: RawStatement, d
|
||||
function topLevelSoftStatementLineStarts(sql: string, statement: RawStatement, databaseType?: DatabaseType): Array<{ hitFrom: number; from: number; keyword: string }> {
|
||||
const starts: Array<{ hitFrom: number; from: number; keyword: string }> = [];
|
||||
const len = statement.to;
|
||||
const explainOptionsStart = explainOptionsParenAt(sql, statement.from);
|
||||
// Recover soft statement boundaries while the user is still typing an
|
||||
// EXPLAIN option list; otherwise its unmatched opener hides every later line.
|
||||
const unclosedExplainOptionsStart = explainOptionsStart !== null && skipBalancedParens(sql, explainOptionsStart) === null ? explainOptionsStart : null;
|
||||
let state: QuoteState | "lineComment" | "blockComment" = "none";
|
||||
let dollarTag = "";
|
||||
let parenDepth = 0;
|
||||
@@ -702,7 +714,7 @@ function topLevelSoftStatementLineStarts(sql: string, statement: RawStatement, d
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (ch === "(") {
|
||||
if (ch === "(" && i !== unclosedExplainOptionsStart) {
|
||||
parenDepth += 1;
|
||||
} else if (ch === ")" && parenDepth > 0) {
|
||||
parenDepth -= 1;
|
||||
@@ -926,6 +938,15 @@ function isExplainLikeKeyword(keyword: string | null): boolean {
|
||||
return keyword === "EXPLAIN" || keyword === "DESCRIBE" || keyword === "DESC";
|
||||
}
|
||||
|
||||
function explainOptionsParenAt(sql: string, pos: number): number | null {
|
||||
const prefixMatch = /^[A-Za-z_][\w$]*/.exec(sql.slice(pos));
|
||||
if (prefixMatch?.[0]?.toUpperCase() !== "EXPLAIN") return null;
|
||||
|
||||
let i = pos + prefixMatch[0].length;
|
||||
while (i < sql.length && isSqlWhitespace(sql[i])) i += 1;
|
||||
return sql[i] === "(" ? i : null;
|
||||
}
|
||||
|
||||
function explainLikeTargetKeywordAt(sql: string, pos: number): string | null {
|
||||
const prefixMatch = /^[A-Za-z_][\w$]*/.exec(sql.slice(pos));
|
||||
const prefix = prefixMatch?.[0]?.toUpperCase();
|
||||
@@ -933,11 +954,93 @@ function explainLikeTargetKeywordAt(sql: string, pos: number): string | null {
|
||||
|
||||
let i = pos + (prefixMatch?.[0].length ?? 0);
|
||||
while (i < sql.length && isSqlWhitespace(sql[i])) i += 1;
|
||||
// Parenthesized EXPLAIN options (e.g. Postgres `EXPLAIN (ANALYZE, BUFFERS) ...`)
|
||||
// sit between the keyword and its target statement. DESC/DESCRIBE take no
|
||||
// options — a paren there is a subquery (ClickHouse `DESCRIBE (SELECT ...)`).
|
||||
if (prefix === "EXPLAIN" && sql[i] === "(") {
|
||||
const optionsEnd = skipBalancedParens(sql, i);
|
||||
if (optionsEnd === null) return null;
|
||||
i = optionsEnd;
|
||||
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 skipBalancedParens(sql: string, pos: number): number | null {
|
||||
let state: "none" | "single" | "double" | "lineComment" | "blockComment" = "none";
|
||||
let depth = 0;
|
||||
let i = pos;
|
||||
|
||||
while (i < sql.length) {
|
||||
const ch = sql[i];
|
||||
const next = sql[i + 1] ?? "";
|
||||
|
||||
if (state === "lineComment") {
|
||||
if (ch === "\n") state = "none";
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (state === "blockComment") {
|
||||
if (ch === "*" && next === "/") {
|
||||
state = "none";
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (state === "single") {
|
||||
if (ch === "'" && next === "'") {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (ch === "'") state = "none";
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (state === "double") {
|
||||
if (ch === '"' && next === '"') {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (ch === '"') state = "none";
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch === "-" && next === "-") {
|
||||
state = "lineComment";
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (ch === "/" && next === "*") {
|
||||
state = "blockComment";
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
if (ch === "'") {
|
||||
state = "single";
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === '"') {
|
||||
state = "double";
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (ch === "(") depth += 1;
|
||||
if (ch === ")") {
|
||||
depth -= 1;
|
||||
if (depth === 0) return i + 1;
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function startsLineComment(sql: string, pos: number): boolean {
|
||||
return (sql[pos] === "-" && sql[pos + 1] === "-") || sql[pos] === "#";
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user