pub(crate) fn count(sql: &str) -> Option<usize> {
let bytes = sql.as_bytes();
let mut statements = 0;
let mut has_text = false;
let mut at = 0;
while let Some(&byte) = bytes.get(at) {
let next = bytes.get(at + 1).copied();
at = match (byte, next) {
(b';', _) => {
statements += usize::from(has_text);
has_text = false;
at + 1
}
(b'-', Some(b'-')) => line_end(bytes, at),
(b'/', Some(b'*')) => block_end(bytes, at)?,
(b, _) if b.is_ascii_whitespace() => at + 1,
_ => {
has_text = true;
token_end(bytes, at)?
}
};
}
Some(statements + usize::from(has_text))
}
fn token_end(bytes: &[u8], at: usize) -> Option<usize> {
match (bytes.get(at), bytes.get(at + 1)) {
(Some(b'\''), _) => quote_end(bytes, at + 1, b'\'', false),
(Some(b'"'), _) => quote_end(bytes, at + 1, b'"', false),
(Some(b'E' | b'e'), Some(b'\'')) => quote_end(bytes, at + 2, b'\'', true),
(Some(b'$'), _) => dollar_end(bytes, at),
(Some(&b), _) if is_word_start(b) => {
let mut end = at + 1;
while bytes.get(end).is_some_and(|&b| is_word(b) || b == b'$') {
end += 1;
}
Some(end)
}
_ => Some(at + 1),
}
}
const fn is_word_start(byte: u8) -> bool {
byte.is_ascii_alphabetic() || byte == b'_' || byte >= 0x80
}
const fn is_word(byte: u8) -> bool {
is_word_start(byte) || byte.is_ascii_digit()
}
fn quote_end(bytes: &[u8], from: usize, quote: u8, backslash: bool) -> Option<usize> {
let mut at = from;
while let Some(&byte) = bytes.get(at) {
if backslash && byte == b'\\' {
at += 2;
} else if byte == quote {
if bytes.get(at + 1) != Some("e) {
return Some(at + 1);
}
at += 2;
} else {
at += 1;
}
}
None
}
fn dollar_end(bytes: &[u8], at: usize) -> Option<usize> {
let mut tag_end = at + 1;
if bytes.get(tag_end).is_some_and(|&b| is_word_start(b)) {
while bytes.get(tag_end).is_some_and(|&b| is_word(b)) {
tag_end += 1;
}
}
if bytes.get(tag_end) != Some(&b'$') {
return Some(at + 1);
}
let tag = &bytes[at..=tag_end];
bytes[tag_end + 1..]
.windows(tag.len())
.position(|window| window == tag)
.map(|offset| tag_end + 1 + offset + tag.len())
}
fn line_end(bytes: &[u8], at: usize) -> usize {
bytes[at..]
.iter()
.position(|&b| b == b'\n' || b == b'\r')
.map_or(bytes.len(), |offset| at + offset + 1)
}
fn block_end(bytes: &[u8], at: usize) -> Option<usize> {
let mut depth = 0_usize;
let mut at = at;
while at < bytes.len() {
match (bytes.get(at), bytes.get(at + 1)) {
(Some(b'/'), Some(b'*')) => {
depth += 1;
at += 2;
}
(Some(b'*'), Some(b'/')) => {
depth -= 1;
at += 2;
if depth == 0 {
return Some(at);
}
}
_ => at += 1,
}
}
None
}
#[cfg(test)]
mod tests;