fix(sql): split current statement on blank lines

This commit is contained in:
t8y2 2026-06-17 15:54:57 +08:00
parent 6e7035823a
commit b0cc3ed0d6
1 changed files with 129 additions and 7 deletions

View File

@ -398,28 +398,31 @@ pub fn find_statement_at_cursor_with_options(sql: &str, cursor_pos: usize, optio
for (idx, statement) in statements.iter().enumerate() {
if cursor > statement.start && cursor < statement.end {
return statement.text.clone();
return statement_text_at_cursor(sql, statement, cursor, options);
}
if cursor == statement.start {
if cursor_has_sql_after_cursor_on_line(sql, cursor) {
return statement.text.clone();
return statement_text_at_cursor(sql, statement, cursor, options);
}
if let Some(prev) = idx.checked_sub(1).and_then(|prev_idx| statements.get(prev_idx)) {
return prev.text.clone();
return statement_text_at_cursor(sql, prev, cursor, options);
}
return statement.text.clone();
return statement_text_at_cursor(sql, statement, cursor, options);
}
if cursor < statement.start {
if let Some(prev) = idx.checked_sub(1).and_then(|prev_idx| statements.get(prev_idx)) {
return prev.text.clone();
return statement_text_at_cursor(sql, prev, cursor, options);
}
return statement.text.clone();
return statement_text_at_cursor(sql, statement, cursor, options);
}
}
statements.last().map(|statement| statement.text.clone()).unwrap_or_else(|| sql.trim().to_string())
statements
.last()
.map(|statement| statement_text_at_cursor(sql, statement, cursor, options))
.unwrap_or_else(|| sql.trim().to_string())
}
fn cursor_has_sql_after_cursor_on_line(sql: &str, cursor: usize) -> bool {
@ -427,6 +430,109 @@ fn cursor_has_sql_after_cursor_on_line(sql: &str, cursor: usize) -> bool {
sql[cursor..line_end].chars().any(|ch| !ch.is_whitespace())
}
fn statement_text_at_cursor(
sql: &str,
statement: &SqlStatementRange,
cursor: usize,
options: SqlParsingOptions,
) -> String {
let soft_ranges = split_statement_range_at_blank_lines(sql, statement, options);
find_statement_text_in_ranges(sql, &soft_ranges, cursor).unwrap_or_else(|| statement.text.clone())
}
fn find_statement_text_in_ranges(sql: &str, ranges: &[SqlStatementRange], cursor: usize) -> Option<String> {
for (idx, range) in ranges.iter().enumerate() {
if cursor > range.start && cursor < range.end {
return Some(range.text.clone());
}
if cursor == range.start {
if cursor_has_sql_after_cursor_on_line(sql, cursor) {
return Some(range.text.clone());
}
if let Some(prev) = idx.checked_sub(1).and_then(|prev_idx| ranges.get(prev_idx)) {
return Some(prev.text.clone());
}
return Some(range.text.clone());
}
if cursor < range.start {
if let Some(prev) = idx.checked_sub(1).and_then(|prev_idx| ranges.get(prev_idx)) {
return Some(prev.text.clone());
}
return Some(range.text.clone());
}
}
ranges.last().map(|range| range.text.clone())
}
fn split_statement_range_at_blank_lines(
sql: &str,
statement: &SqlStatementRange,
options: SqlParsingOptions,
) -> Vec<SqlStatementRange> {
if options.supports_oracle_plsql_blocks && starts_with_oracle_plsql_block(&statement.text) {
return vec![statement.clone()];
}
let mut ranges = Vec::new();
let mut scanner = SqlScanner::default();
let mut current_start = statement.start;
let mut line_start = statement.start;
let mut line_has_non_whitespace = false;
let mut blank_line_run = 0usize;
for (relative_idx, ch) in sql[statement.start..statement.end].char_indices() {
let idx = statement.start + relative_idx;
if ch == '\n' {
if !line_has_non_whitespace && !scanner.is_masked() {
blank_line_run += 1;
} else {
blank_line_run = 0;
}
scanner.step(sql, idx, ch);
line_start = idx + ch.len_utf8();
line_has_non_whitespace = false;
continue;
}
if !line_has_non_whitespace && !ch.is_whitespace() {
if blank_line_run >= 2
&& !scanner.is_masked()
&& has_executable_sql_with_options(&sql[current_start..line_start], options)
&& starts_with_soft_statement_keyword(&sql[line_start..statement.end], options)
{
push_statement_range(&mut ranges, sql, current_start, line_start, options);
current_start = line_start;
}
blank_line_run = 0;
line_has_non_whitespace = true;
}
scanner.step(sql, idx, ch);
}
push_statement_range(&mut ranges, sql, current_start, statement.end, options);
if ranges.is_empty() {
vec![statement.clone()]
} else {
ranges
}
}
fn starts_with_soft_statement_keyword(sql: &str, options: SqlParsingOptions) -> bool {
starts_with_executable_sql_keyword_with_options(
sql,
&[
"CREATE", "ALTER", "DROP", "INSERT", "UPDATE", "DELETE", "MERGE", "REPLACE", "TRUNCATE", "GRANT", "REVOKE",
"COMMENT", "EXPLAIN", "SHOW", "DESCRIBE", "USE", "SET", "CALL", "EXEC", "EXECUTE", "BEGIN", "COMMIT",
"ROLLBACK", "DECLARE", "ANALYZE", "VACUUM", "PRAGMA", "REFRESH", "COPY",
],
options,
)
}
#[allow(dead_code)]
fn split_sql_statement_ranges(sql: &str) -> Vec<SqlStatementRange> {
split_sql_statement_ranges_with_options(sql, SqlParsingOptions::default())
@ -2218,6 +2324,22 @@ SELECT 1;";
assert_eq!(super::find_statement_at_cursor(sql, cursor), "SELECT 2");
}
#[test]
fn finds_statement_at_cursor_after_double_blank_line_without_semicolon() {
let sql = "SELECT * FROM old_table\n\n\nCREATE VIEW v AS SELECT 1";
let cursor = sql[..sql.find("CREATE VIEW").unwrap()].encode_utf16().count();
assert_eq!(super::find_statement_at_cursor(sql, cursor), "CREATE VIEW v AS SELECT 1");
}
#[test]
fn keeps_create_view_statement_together_across_double_blank_line() {
let sql = "CREATE VIEW v AS\n\n\nSELECT 1";
let cursor = sql[..sql.find("SELECT 1").unwrap()].encode_utf16().count();
assert_eq!(super::find_statement_at_cursor(sql, cursor), "CREATE VIEW v AS\n\n\nSELECT 1");
}
#[test]
fn finds_statement_with_dollar_quote() {
let sql = "SELECT $$a;b$$; SELECT 2";