feat(dameng): full DM8 database support
Adds Dameng DM8 explain/autotrace support and user-management SQL support, with backend autotrace safety checks and escaped CREATE USER SQL.
This commit is contained in:
1 parent
a5659d7637
commit
897e6998fd
22 files changed
+1154
-55
No files matched your search
@@ -43,6 +43,8 @@ pub fn build_explain_sql(options: ExplainSqlOptions) -> ExplainSqlBuildResult {
|
||||
|
||||
let sql = if options.database_type == Some(DatabaseType::Postgres) {
|
||||
format!("EXPLAIN (FORMAT JSON) {source}")
|
||||
} else if options.database_type == Some(DatabaseType::Dameng) {
|
||||
format!("EXPLAIN {source}")
|
||||
} else {
|
||||
format!("EXPLAIN FORMAT=JSON {source}")
|
||||
};
|
||||
@@ -69,7 +71,15 @@ pub fn build_dropped_file_preview_sql(options: DroppedFilePreviewSqlOptions) ->
|
||||
}
|
||||
|
||||
pub fn supports_explain_plan(database_type: Option<DatabaseType>) -> bool {
|
||||
matches!(database_type, Some(DatabaseType::Mysql | DatabaseType::Postgres))
|
||||
matches!(database_type, Some(DatabaseType::Mysql | DatabaseType::Postgres | DatabaseType::Dameng))
|
||||
}
|
||||
|
||||
pub fn is_safe_dameng_autotrace_sql(sql: &str) -> bool {
|
||||
let source = strip_trailing_semicolons(sql.trim());
|
||||
if source.is_empty() || has_extra_statement_after_semicolon(&source) {
|
||||
return false;
|
||||
}
|
||||
is_safe_explain_source(&source) && !contains_dangerous_sql_keyword(&source)
|
||||
}
|
||||
|
||||
fn explain_err(reason: &str) -> ExplainSqlBuildResult {
|
||||
@@ -87,6 +97,42 @@ fn is_safe_explain_source(sql: &str) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
fn contains_dangerous_sql_keyword(sql: &str) -> bool {
|
||||
let source = strip_sql_comments_and_literals(sql).to_lowercase();
|
||||
["drop", "delete", "truncate", "alter", "update", "merge", "replace", "insert", "create"]
|
||||
.iter()
|
||||
.any(|keyword| contains_word(&source, keyword))
|
||||
}
|
||||
|
||||
fn contains_word(source: &str, word: &str) -> bool {
|
||||
let bytes = source.as_bytes();
|
||||
let word_bytes = word.as_bytes();
|
||||
if word_bytes.is_empty() || bytes.len() < word_bytes.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
for idx in 0..=bytes.len() - word_bytes.len() {
|
||||
if &bytes[idx..idx + word_bytes.len()] != word_bytes {
|
||||
continue;
|
||||
}
|
||||
let before = idx.checked_sub(1).and_then(|i| bytes.get(i)).copied();
|
||||
let after = bytes.get(idx + word_bytes.len()).copied();
|
||||
if !is_identifier_byte(before) && !is_identifier_byte(after) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn is_identifier_byte(byte: Option<u8>) -> bool {
|
||||
byte.is_some_and(|b| b.is_ascii_alphanumeric() || b == b'_')
|
||||
}
|
||||
|
||||
fn has_extra_statement_after_semicolon(sql: &str) -> bool {
|
||||
let stripped = strip_sql_comments_and_literals(sql);
|
||||
stripped.split(';').skip(1).any(|part| !part.trim().is_empty())
|
||||
}
|
||||
|
||||
fn strip_sql_comments(sql: &str) -> String {
|
||||
let mut output = String::with_capacity(sql.len());
|
||||
let mut chars = sql.chars().peekable();
|
||||
@@ -132,6 +178,87 @@ fn strip_sql_comments(sql: &str) -> String {
|
||||
output
|
||||
}
|
||||
|
||||
fn strip_sql_comments_and_literals(sql: &str) -> String {
|
||||
let mut output = String::with_capacity(sql.len());
|
||||
let mut chars = sql.chars().peekable();
|
||||
let mut in_line_comment = false;
|
||||
let mut in_block_comment = false;
|
||||
let mut in_single_quote = false;
|
||||
let mut in_double_quote = false;
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
if in_line_comment {
|
||||
if ch == '\n' {
|
||||
in_line_comment = false;
|
||||
output.push(' ');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if in_block_comment {
|
||||
if ch == '*' && chars.peek() == Some(&'/') {
|
||||
chars.next();
|
||||
in_block_comment = false;
|
||||
output.push(' ');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if in_single_quote {
|
||||
if ch == '\'' {
|
||||
if chars.peek() == Some(&'\'') {
|
||||
chars.next();
|
||||
} else {
|
||||
in_single_quote = false;
|
||||
}
|
||||
}
|
||||
output.push(' ');
|
||||
continue;
|
||||
}
|
||||
|
||||
if in_double_quote {
|
||||
if ch == '"' {
|
||||
if chars.peek() == Some(&'"') {
|
||||
chars.next();
|
||||
} else {
|
||||
in_double_quote = false;
|
||||
}
|
||||
}
|
||||
output.push(' ');
|
||||
continue;
|
||||
}
|
||||
|
||||
if ch == '-' && chars.peek() == Some(&'-') {
|
||||
chars.next();
|
||||
in_line_comment = true;
|
||||
continue;
|
||||
}
|
||||
if ch == '#' {
|
||||
in_line_comment = true;
|
||||
continue;
|
||||
}
|
||||
if ch == '/' && chars.peek() == Some(&'*') {
|
||||
chars.next();
|
||||
in_block_comment = true;
|
||||
continue;
|
||||
}
|
||||
if ch == '\'' {
|
||||
in_single_quote = true;
|
||||
output.push(' ');
|
||||
continue;
|
||||
}
|
||||
if ch == '"' {
|
||||
in_double_quote = true;
|
||||
output.push(' ');
|
||||
continue;
|
||||
}
|
||||
|
||||
output.push(ch);
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -153,6 +280,33 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_dameng_explain_sql() {
|
||||
let result = build_explain_sql(ExplainSqlOptions {
|
||||
database_type: Some(DatabaseType::Dameng),
|
||||
sql: "SELECT * FROM t1 WHERE id = 1".to_string(),
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
result,
|
||||
ExplainSqlBuildResult {
|
||||
ok: true,
|
||||
sql: Some("EXPLAIN SELECT * FROM t1 WHERE id = 1".to_string()),
|
||||
reason: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validates_dameng_autotrace_sql_safety() {
|
||||
assert!(is_safe_dameng_autotrace_sql("SELECT * FROM t WHERE name = 'delete';"));
|
||||
assert!(is_safe_dameng_autotrace_sql("/* comment */ WITH q AS (SELECT 1) SELECT * FROM q"));
|
||||
assert!(!is_safe_dameng_autotrace_sql("SELECT * FROM t; DELETE FROM t"));
|
||||
assert!(!is_safe_dameng_autotrace_sql("UPDATE t SET name = 'x'"));
|
||||
assert!(!is_safe_dameng_autotrace_sql("SELECT * FROM t; /* hidden */ DROP TABLE t"));
|
||||
assert!(!is_safe_dameng_autotrace_sql(""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_mysql_json_explain_and_rejects_unsafe_sql() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in new issue
Block a user