#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Statement {
pub text: String,
pub line: usize,
}
pub fn split(sql: &str) -> Vec<Statement> {
let b = sql.as_bytes();
let mut out = Vec::new();
let mut start = 0;
let mut i = 0;
while i < b.len() {
match b[i] {
b'\'' | b'"' | b'`' => i = skip_quoted(b, i),
b'-' if b.get(i + 1) == Some(&b'-') => {
while i < b.len() && b[i] != b'\n' {
i += 1;
}
}
b'/' if b.get(i + 1) == Some(&b'*') => i = skip_block_comment(b, i),
b'$' => i = skip_dollar_quoted(sql, i),
b';' => {
push_statement(sql, start, i, &mut out);
i += 1;
start = i;
}
_ => i += 1,
}
}
push_statement(sql, start, b.len(), &mut out);
out
}
fn push_statement(sql: &str, start: usize, end: usize, out: &mut Vec<Statement>) {
let raw = &sql[start..end];
let text = raw.trim();
if text.is_empty() || strip_leading_comments(text).is_empty() {
return;
}
let lead = raw.len() - raw.trim_start().len();
let line = sql[..start + lead].matches('\n').count() + 1;
out.push(Statement {
text: text.to_string(),
line,
});
}
fn skip_quoted(b: &[u8], i: usize) -> usize {
let q = b[i];
let mut j = i + 1;
while j < b.len() {
if b[j] == q {
if b.get(j + 1) == Some(&q) {
j += 2;
continue;
}
return j + 1;
}
j += 1;
}
b.len()
}
fn skip_block_comment(b: &[u8], i: usize) -> usize {
let mut depth = 0;
let mut j = i;
while j < b.len() {
if b[j] == b'/' && b.get(j + 1) == Some(&b'*') {
depth += 1;
j += 2;
} else if b[j] == b'*' && b.get(j + 1) == Some(&b'/') {
depth -= 1;
j += 2;
if depth == 0 {
return j;
}
} else {
j += 1;
}
}
b.len()
}
fn skip_dollar_quoted(sql: &str, i: usize) -> usize {
let b = sql.as_bytes();
let mut j = i + 1;
if j < b.len() && (b[j].is_ascii_alphabetic() || b[j] == b'_') {
while j < b.len() && (b[j].is_ascii_alphanumeric() || b[j] == b'_') {
j += 1;
}
}
if b.get(j) != Some(&b'$') {
return i + 1;
}
let tag = &sql[i..=j];
match sql[j + 1..].find(tag) {
Some(pos) => j + 1 + pos + tag.len(),
None => b.len(),
}
}
pub fn strip_leading_comments(mut s: &str) -> &str {
loop {
s = s.trim_start();
if let Some(rest) = s.strip_prefix("--") {
s = rest.find('\n').map_or("", |p| &rest[p..]);
} else if s.starts_with("/*") {
let end = skip_block_comment(s.as_bytes(), 0);
s = &s[end..];
} else {
return s;
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StatementKind {
Read,
TempCreate,
Other,
}
impl StatementKind {
pub fn is_read_only_safe(self) -> bool {
matches!(self, StatementKind::Read | StatementKind::TempCreate)
}
}
pub fn classify(statement: &str) -> StatementKind {
let s = strip_leading_comments(statement).trim_start_matches(|c: char| c == '(' || c.is_whitespace());
let words: Vec<String> = s
.split_whitespace()
.take(5)
.map(|w| w.trim_matches('(').to_ascii_lowercase())
.collect();
let w = |n: usize| words.get(n).map(String::as_str).unwrap_or("");
match w(0) {
"select" | "with" => StatementKind::Read,
"create" => {
let mut k = 1;
if w(1) == "or" && w(2) == "replace" {
k = 3;
}
if matches!(w(k), "temp" | "temporary") && matches!(w(k + 1), "table" | "view") {
StatementKind::TempCreate
} else {
StatementKind::Other
}
}
_ => StatementKind::Other,
}
}