fix(saphana): keep DO blocks together
This commit is contained in:
parent
f41c7dd6a0
commit
6e7f0578b6
|
|
@ -85,6 +85,12 @@ BEGIN
|
|||
END;
|
||||
SELECT 2;`;
|
||||
|
||||
const sapHanaDoBlockFixture = `DO
|
||||
BEGIN
|
||||
SELECT 1 AS "Result" FROM DUMMY;
|
||||
END;
|
||||
SELECT 2 FROM DUMMY;`;
|
||||
|
||||
describe("splitSqlStatementRanges", () => {
|
||||
it("splits multiple top-level statements", () => {
|
||||
const sql = "SELECT 1;\nSELECT 2;\nSELECT 3;";
|
||||
|
|
@ -177,6 +183,14 @@ describe("splitSqlStatementRanges", () => {
|
|||
expect(ranges[0].sql).toContain("END;");
|
||||
expect(ranges[0].sql).not.toContain("\n/");
|
||||
});
|
||||
|
||||
it("keeps SAP HANA DO blocks together", () => {
|
||||
const ranges = splitSqlStatementRanges(sapHanaDoBlockFixture, "saphana");
|
||||
|
||||
expect(rangeSqlTexts(ranges)).toEqual([sapHanaDoBlockFixture.slice(0, sapHanaDoBlockFixture.indexOf("\nSELECT 2")), "SELECT 2 FROM DUMMY"]);
|
||||
expect(ranges[0].sql).toContain('SELECT 1 AS "Result" FROM DUMMY;');
|
||||
expect(ranges[0].sql).toContain("END;");
|
||||
});
|
||||
});
|
||||
|
||||
describe("statementRangeAtCursor", () => {
|
||||
|
|
@ -423,6 +437,12 @@ WHERE request_json LIKE '%"paperFlag":null%';`;
|
|||
const range = statementRangeAtCursor(oraclePlSqlFixture, indexOf(oraclePlSqlFixture, "ORDERS_10K", 2), "oracle");
|
||||
expect(range?.sql.trim()).toBe(oraclePlSqlFixture.slice(0, oraclePlSqlFixture.indexOf("\n/")));
|
||||
});
|
||||
|
||||
it("returns the full SAP HANA DO block for cursors inside nested statements", () => {
|
||||
const range = statementRangeAtCursor(sapHanaDoBlockFixture, indexOf(sapHanaDoBlockFixture, "Result"), "saphana");
|
||||
|
||||
expect(range?.sql.trim()).toBe(sapHanaDoBlockFixture.slice(0, sapHanaDoBlockFixture.indexOf("\nSELECT 2")));
|
||||
});
|
||||
});
|
||||
|
||||
describe("executableStatementRanges", () => {
|
||||
|
|
@ -465,6 +485,10 @@ describe("executableStatementRanges", () => {
|
|||
expect(rangeSqlTexts(executableStatementRanges(mysqlRoutineWithLoopsFixture, "mysql"))).toEqual([mysqlRoutineWithLoopsFixture.slice(0, mysqlRoutineWithLoopsFixture.indexOf("\nSELECT 2;")).replace(/;$/, "").trim(), "SELECT 2"]);
|
||||
});
|
||||
|
||||
it("does not split executable SAP HANA DO ranges at inner statements", () => {
|
||||
expect(rangeSqlTexts(executableStatementRanges(sapHanaDoBlockFixture, "saphana"))).toEqual([sapHanaDoBlockFixture.slice(0, sapHanaDoBlockFixture.indexOf("\nSELECT 2")), "SELECT 2 FROM DUMMY"]);
|
||||
});
|
||||
|
||||
it("returns executable SQL Server batches without GO delimiter lines", () => {
|
||||
expect(rangeSqlTexts(executableStatementRanges("SELECT 1\nGO\nSELECT 2;", "sqlserver"))).toEqual(["SELECT 1", "SELECT 2"]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -80,6 +80,7 @@ const DATABASE_SOFT_STATEMENT_KEYWORDS: Partial<Record<DatabaseType, readonly st
|
|||
duckdb: ["ATTACH", "DETACH", "EXPORT", "IMPORT", "INSTALL", "LOAD"],
|
||||
clickhouse: ["ATTACH", "CHECK", "DETACH", "EXCHANGE", "KILL", "OPTIMIZE", "SYSTEM"],
|
||||
sqlserver: ["BACKUP", "DBCC", "DENY", "RESTORE"],
|
||||
saphana: ["DO"],
|
||||
oracle: ["FLASHBACK", "LOCK", "PURGE"],
|
||||
dameng: ["FLASHBACK", "LOCK", "PURGE"],
|
||||
gaussdb: ["DO", "LOCK"],
|
||||
|
|
@ -111,6 +112,7 @@ const MYSQL_CONTROL_BLOCK_SUFFIXES = new Set(["IF", "LOOP", "CASE", "REPEAT", "W
|
|||
const ORACLE_PL_SQL_BLOCK_STARTERS = new Set(["DECLARE", "BEGIN"]);
|
||||
const ORACLE_PL_SQL_CREATE_OBJECT_TYPES = new Set(["FUNCTION", "PROCEDURE", "TRIGGER", "PACKAGE", "PACKAGE BODY", "TYPE", "TYPE BODY"]);
|
||||
const ORACLE_PL_SQL_TERMINATORS = new Set(["IF", "LOOP", "CASE"]);
|
||||
const SAP_HANA_SCRIPT_BLOCK_TERMINATORS = new Set(["IF", "FOR", "WHILE"]);
|
||||
|
||||
/**
|
||||
* Parse the SQL document into top-level statement ranges delimited by `;`.
|
||||
|
|
@ -334,10 +336,16 @@ export function splitSqlStatementRanges(sql: string, databaseType?: DatabaseType
|
|||
// Internal semicolons remain part of the routine body.
|
||||
flush();
|
||||
} else {
|
||||
const isOraclePlSql = isOracleLikeDatabase(databaseType) && statementStart !== -1 && startsWithOraclePlSqlBlock(sql.slice(statementStart, i));
|
||||
if (isOraclePlSql) {
|
||||
const statementSoFar = statementStart === -1 ? "" : sql.slice(statementStart, i);
|
||||
const isOraclePlSql = isOracleLikeDatabase(databaseType) && statementStart !== -1 && startsWithOraclePlSqlBlock(statementSoFar);
|
||||
const isSapHanaScriptBlock = isSapHanaScriptBlockDatabase(databaseType) && statementStart !== -1 && startsWithSapHanaScriptBlock(statementSoFar);
|
||||
if (isOraclePlSql || isSapHanaScriptBlock) {
|
||||
markContent(i);
|
||||
if (!oraclePlSqlBlockIsComplete(sql.slice(statementStart, i + 1))) {
|
||||
if (isOraclePlSql && !oraclePlSqlBlockIsComplete(sql.slice(statementStart, i + 1))) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (isSapHanaScriptBlock && !sapHanaScriptBlockIsComplete(sql.slice(statementStart, i + 1))) {
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
|
|
@ -459,6 +467,7 @@ function rangeForCursorInSoftRanges(sql: string, ranges: RawStatement[], pos: nu
|
|||
|
||||
function splitStatementRangeAtSoftStarts(sql: string, statement: RawStatement, databaseType?: DatabaseType): RawStatement[] {
|
||||
if (isOraclePlSqlStatement(statement.sql, databaseType)) return [statement];
|
||||
if (isSapHanaScriptBlockStatement(statement.sql, databaseType)) return [statement];
|
||||
|
||||
const lineStarts = topLevelSoftStatementLineStarts(sql, statement, databaseType);
|
||||
if (lineStarts.length <= 1) return [statement];
|
||||
|
|
@ -1082,6 +1091,14 @@ export function isOraclePlSqlStatement(sql: string, databaseType?: DatabaseType)
|
|||
return isOracleLikeDatabase(databaseType) && startsWithOraclePlSqlBlock(sql);
|
||||
}
|
||||
|
||||
function isSapHanaScriptBlockDatabase(databaseType?: DatabaseType): boolean {
|
||||
return databaseType === "saphana";
|
||||
}
|
||||
|
||||
function isSapHanaScriptBlockStatement(sql: string, databaseType?: DatabaseType): boolean {
|
||||
return isSapHanaScriptBlockDatabase(databaseType) && startsWithSapHanaScriptBlock(sql);
|
||||
}
|
||||
|
||||
function isMysqlRoutineBlockDatabase(databaseType?: DatabaseType): boolean {
|
||||
return !!databaseType && MYSQL_ROUTINE_BLOCK_DATABASES.has(databaseType);
|
||||
}
|
||||
|
|
@ -1255,6 +1272,42 @@ function startsWithOraclePlSqlBlock(sql: string): boolean {
|
|||
return ORACLE_PL_SQL_CREATE_OBJECT_TYPES.has(words[index] ?? "");
|
||||
}
|
||||
|
||||
function startsWithSapHanaScriptBlock(sql: string): boolean {
|
||||
return oraclePlSqlWords(sql)[0] === "DO";
|
||||
}
|
||||
|
||||
function sapHanaScriptBlockIsComplete(sql: string): boolean {
|
||||
if (!startsWithSapHanaScriptBlock(sql)) return false;
|
||||
|
||||
const tokens = oraclePlSqlTokens(sql);
|
||||
const stack: string[] = [];
|
||||
let sawBegin = false;
|
||||
|
||||
for (let index = 0; index < tokens.length; index += 1) {
|
||||
const token = tokens[index];
|
||||
if (token.kind !== "word") continue;
|
||||
|
||||
if (token.value === "BEGIN") {
|
||||
if (previousWordToken(tokens, index) === "END") continue;
|
||||
stack.push("BLOCK");
|
||||
sawBegin = true;
|
||||
continue;
|
||||
}
|
||||
if (token.value === "IF" || token.value === "FOR" || token.value === "WHILE" || token.value === "CASE") {
|
||||
if (previousWordToken(tokens, index) !== "END") stack.push(token.value);
|
||||
continue;
|
||||
}
|
||||
if (token.value === "END") {
|
||||
const next = nextWordToken(tokens, index);
|
||||
const top = stack[stack.length - 1];
|
||||
const target = SAP_HANA_SCRIPT_BLOCK_TERMINATORS.has(next ?? "") ? next : top === "CASE" ? "CASE" : "BLOCK";
|
||||
if (top === target) stack.pop();
|
||||
}
|
||||
}
|
||||
|
||||
return sawBegin && stack.length === 0 && tokens[tokens.length - 1]?.kind === "semicolon";
|
||||
}
|
||||
|
||||
function oraclePlSqlBlockIsComplete(sql: string): boolean {
|
||||
const tokens = oraclePlSqlTokens(sql);
|
||||
if (!startsWithOraclePlSqlBlock(sql)) return false;
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@ struct SqlDialectProfile {
|
|||
supports_custom_delimiter_commands: bool,
|
||||
supports_mysql_routine_blocks: bool,
|
||||
supports_dollar_quoted_strings: bool,
|
||||
supports_hana_do_blocks: bool,
|
||||
supports_go_batch_separator: bool,
|
||||
keeps_sqlserver_module_batch_at_cursor: bool,
|
||||
}
|
||||
|
|
@ -124,6 +125,7 @@ impl Default for SqlDialectProfile {
|
|||
supports_custom_delimiter_commands: true,
|
||||
supports_mysql_routine_blocks: false,
|
||||
supports_dollar_quoted_strings: true,
|
||||
supports_hana_do_blocks: false,
|
||||
supports_go_batch_separator: false,
|
||||
keeps_sqlserver_module_batch_at_cursor: false,
|
||||
}
|
||||
|
|
@ -144,6 +146,10 @@ impl SqlDialectProfile {
|
|||
return Self::mysql_compatible();
|
||||
}
|
||||
|
||||
if matches!(db_type, DatabaseType::SapHana) {
|
||||
return Self::sap_hana();
|
||||
}
|
||||
|
||||
Self::default()
|
||||
}
|
||||
|
||||
|
|
@ -159,6 +165,10 @@ impl SqlDialectProfile {
|
|||
Self { supports_go_batch_separator: true, keeps_sqlserver_module_batch_at_cursor: true, ..Self::default() }
|
||||
}
|
||||
|
||||
fn sap_hana() -> Self {
|
||||
Self { supports_hana_do_blocks: true, ..Self::default() }
|
||||
}
|
||||
|
||||
fn is_mysql_compatible_database(db_type: DatabaseType) -> bool {
|
||||
matches!(
|
||||
db_type,
|
||||
|
|
@ -409,6 +419,11 @@ impl SqlStatementSplitter {
|
|||
if oracle_plsql_block_is_complete(&self.buffer) {
|
||||
self.push_current_statement(&mut statements);
|
||||
}
|
||||
} else if self.options.profile.supports_hana_do_blocks && starts_with_hana_do_block(&self.buffer) {
|
||||
self.buffer.push(ch);
|
||||
if hana_do_block_is_complete(&self.buffer) {
|
||||
self.push_current_statement(&mut statements);
|
||||
}
|
||||
} else {
|
||||
self.push_current_statement(&mut statements);
|
||||
}
|
||||
|
|
@ -623,6 +638,9 @@ fn split_statement_range_at_blank_lines(
|
|||
if options.profile.supports_oracle_plsql_blocks && starts_with_oracle_plsql_block(&statement.text) {
|
||||
return vec![statement.clone()];
|
||||
}
|
||||
if options.profile.supports_hana_do_blocks && starts_with_hana_do_block(&statement.text) {
|
||||
return vec![statement.clone()];
|
||||
}
|
||||
|
||||
let mut ranges = Vec::new();
|
||||
let mut scanner = SqlScanner::with_profile(options.profile);
|
||||
|
|
@ -670,6 +688,10 @@ fn split_statement_range_at_blank_lines(
|
|||
}
|
||||
|
||||
fn starts_with_soft_statement_keyword(sql: &str, options: SqlParsingOptions) -> bool {
|
||||
if options.profile.supports_hana_do_blocks && starts_with_executable_sql_keyword_with_options(sql, &["DO"], options)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
starts_with_executable_sql_keyword_with_options(
|
||||
sql,
|
||||
&[
|
||||
|
|
@ -815,6 +837,12 @@ fn split_sql_statement_ranges_with_options(sql: &str, options: SqlParsingOptions
|
|||
continue;
|
||||
}
|
||||
push_statement_range(&mut ranges, sql, start, i + ch.len_utf8(), options);
|
||||
} else if options.profile.supports_hana_do_blocks && starts_with_hana_do_block(&sql[start..i]) {
|
||||
if !hana_do_block_is_complete(&sql[start..i + ch.len_utf8()]) {
|
||||
i += ch.len_utf8();
|
||||
continue;
|
||||
}
|
||||
push_statement_range(&mut ranges, sql, start, i + ch.len_utf8(), options);
|
||||
} else {
|
||||
push_statement_range(&mut ranges, sql, start, i, options);
|
||||
}
|
||||
|
|
@ -1958,6 +1986,68 @@ fn oracle_plsql_block_is_complete(sql: &str) -> bool {
|
|||
OraclePlSqlBlock::parse(sql).is_complete()
|
||||
}
|
||||
|
||||
fn starts_with_hana_do_block(sql: &str) -> bool {
|
||||
HanaDoBlock::parse(sql).starts_block()
|
||||
}
|
||||
|
||||
fn hana_do_block_is_complete(sql: &str) -> bool {
|
||||
HanaDoBlock::parse(sql).is_complete()
|
||||
}
|
||||
|
||||
struct HanaDoBlock {
|
||||
tokens: Vec<OraclePlSqlToken>,
|
||||
}
|
||||
|
||||
impl HanaDoBlock {
|
||||
fn parse(sql: &str) -> Self {
|
||||
Self { tokens: oracle_plsql_tokens(sql) }
|
||||
}
|
||||
|
||||
fn starts_block(&self) -> bool {
|
||||
self.tokens.first().is_some_and(|token| token.is_word("DO"))
|
||||
}
|
||||
|
||||
fn is_complete(&self) -> bool {
|
||||
if !self.starts_block() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut stack: Vec<String> = Vec::new();
|
||||
let mut saw_begin = false;
|
||||
|
||||
for (index, token) in self.tokens.iter().enumerate() {
|
||||
if token.is_word("BEGIN") {
|
||||
if previous_word_token(&self.tokens, index).is_some_and(|previous| previous == "END") {
|
||||
continue;
|
||||
}
|
||||
stack.push("BLOCK".to_string());
|
||||
saw_begin = true;
|
||||
continue;
|
||||
}
|
||||
if token.is_any_word(&["IF", "FOR", "WHILE", "CASE"]) {
|
||||
if previous_word_token(&self.tokens, index).is_none_or(|previous| previous != "END") {
|
||||
stack.push(token.as_word().unwrap_or("BLOCK").to_string());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if token.is_word("END") {
|
||||
let next = next_word_token(&self.tokens, index);
|
||||
let top = stack.last().map(|value| value.as_str());
|
||||
let target = match next {
|
||||
Some(keyword @ ("IF" | "FOR" | "WHILE")) => keyword,
|
||||
_ if top == Some("CASE") => "CASE",
|
||||
_ => "BLOCK",
|
||||
};
|
||||
if top == Some(target) {
|
||||
stack.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saw_begin && stack.is_empty() && self.tokens.last().is_some_and(OraclePlSqlToken::is_semicolon)
|
||||
}
|
||||
}
|
||||
|
||||
struct OraclePlSqlBlock {
|
||||
tokens: Vec<OraclePlSqlToken>,
|
||||
}
|
||||
|
|
@ -2056,6 +2146,13 @@ impl OraclePlSqlToken {
|
|||
matches!(self, Self::Word(value) if value == expected)
|
||||
}
|
||||
|
||||
fn as_word(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::Word(value) => Some(value),
|
||||
Self::Semicolon => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_any_word(&self, expected: &[&str]) -> bool {
|
||||
expected.iter().any(|word| self.is_word(word))
|
||||
}
|
||||
|
|
@ -2065,6 +2162,14 @@ impl OraclePlSqlToken {
|
|||
}
|
||||
}
|
||||
|
||||
fn previous_word_token(tokens: &[OraclePlSqlToken], index: usize) -> Option<&str> {
|
||||
tokens[..index].iter().rev().find_map(OraclePlSqlToken::as_word)
|
||||
}
|
||||
|
||||
fn next_word_token(tokens: &[OraclePlSqlToken], index: usize) -> Option<&str> {
|
||||
tokens[index + 1..].iter().find_map(OraclePlSqlToken::as_word)
|
||||
}
|
||||
|
||||
fn oracle_plsql_tokens(sql: &str) -> Vec<OraclePlSqlToken> {
|
||||
let dialect = OracleDialect {};
|
||||
if let Ok(tokens) = Tokenizer::new(&dialect, sql).tokenize() {
|
||||
|
|
@ -2817,6 +2922,7 @@ SELECT 2;";
|
|||
assert!(!default.supports_hash_line_comments);
|
||||
assert!(!default.supports_mysql_routine_blocks);
|
||||
assert!(!default.supports_oracle_plsql_blocks);
|
||||
assert!(!default.supports_hana_do_blocks);
|
||||
assert!(!default.supports_slash_line_block_delimiter);
|
||||
assert!(!default.supports_go_batch_separator);
|
||||
assert!(!default.keeps_sqlserver_module_batch_at_cursor);
|
||||
|
|
@ -2848,6 +2954,43 @@ SELECT 2;";
|
|||
assert_eq!(sql_server, SqlDialectProfile::sql_server());
|
||||
assert!(sql_server.supports_go_batch_separator);
|
||||
assert!(sql_server.keeps_sqlserver_module_batch_at_cursor);
|
||||
|
||||
let sap_hana = SqlDialectProfile::for_database_type(DatabaseType::SapHana);
|
||||
assert_eq!(sap_hana, SqlDialectProfile::sap_hana());
|
||||
assert!(sap_hana.supports_hana_do_blocks);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sap_hana_split_keeps_do_block_together() {
|
||||
let sql = "\
|
||||
DO
|
||||
BEGIN
|
||||
SELECT 1 AS \"Result\" FROM DUMMY;
|
||||
END;
|
||||
SELECT 2 FROM DUMMY;";
|
||||
|
||||
assert_eq!(
|
||||
split_sql_statements_for_database(sql, DatabaseType::SapHana),
|
||||
vec!["DO\nBEGIN\n SELECT 1 AS \"Result\" FROM DUMMY;\nEND;", "SELECT 2 FROM DUMMY"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sap_hana_cursor_statement_keeps_nested_do_block_together() {
|
||||
let sql = "\
|
||||
DO
|
||||
BEGIN
|
||||
IF 1 = 1 THEN
|
||||
SELECT CASE WHEN 1 = 1 THEN 1 ELSE 0 END AS \"Result\" FROM DUMMY;
|
||||
END IF;
|
||||
END;
|
||||
SELECT 2 FROM DUMMY;";
|
||||
let cursor = sql.find("Result").unwrap();
|
||||
|
||||
assert_eq!(
|
||||
find_statement_at_cursor_for_database(sql, cursor, DatabaseType::SapHana),
|
||||
"DO\nBEGIN\n IF 1 = 1 THEN\n SELECT CASE WHEN 1 = 1 THEN 1 ELSE 0 END AS \"Result\" FROM DUMMY;\n END IF;\nEND;"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in New Issue