pub(super) fn find_keyword(haystack: &str, keyword: &str) -> Option<usize> {
debug_assert!(keyword.is_ascii(), "keyword {keyword:?} must be ASCII");
let hay = haystack.as_bytes();
let kw = keyword.as_bytes();
if kw.is_empty() || kw.len() > hay.len() {
return None;
}
(0..=hay.len() - kw.len()).find(|&i| hay[i..i + kw.len()].eq_ignore_ascii_case(kw))
}
pub(super) fn find_keyword_end(haystack: &str, keyword: &str) -> Option<usize> {
find_keyword(haystack, keyword).map(|i| i + keyword.len())
}
pub(super) fn starts_with_keyword(haystack: &str, keyword: &str) -> bool {
debug_assert!(keyword.is_ascii(), "keyword {keyword:?} must be ASCII");
haystack.len() >= keyword.len()
&& haystack.as_bytes()[..keyword.len()].eq_ignore_ascii_case(keyword.as_bytes())
}
pub(super) fn contains_keyword(haystack: &str, keyword: &str) -> bool {
find_keyword(haystack, keyword).is_some()
}
pub(super) fn strip_keyword_prefix<'a>(haystack: &'a str, keyword: &str) -> Option<&'a str> {
starts_with_keyword(haystack, keyword).then(|| &haystack[keyword.len()..])
}
pub(super) fn name_after_keyword<'a>(
stmt: &'a str,
keyword: &str,
guards: &[&str],
) -> Option<&'a str> {
let start = find_keyword_end(stmt, keyword)?;
let mut rest = stmt[start..].trim_start();
for guard in guards {
if let Some(stripped) = strip_keyword_prefix(rest, guard) {
rest = stripped.trim_start();
break;
}
}
let end = rest
.find(|c: char| c.is_whitespace() || c == '(' || c == ',' || c == ';')
.unwrap_or(rest.len());
let name = rest[..end].trim().trim_matches('"').trim_matches('`');
(!name.is_empty()).then_some(name)
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn find_keyword_is_case_insensitive() {
assert_eq!(find_keyword("create table t", "TABLE"), Some(7));
assert_eq!(find_keyword("CREATE TABLE t", "table"), Some(7));
assert_eq!(find_keyword("no keyword here", "TABLE"), None);
}
#[test]
fn offsets_index_the_original_string() {
let s = "CONSTRAINT \u{250}\u{250} FOREIGN KEY(a) REFERENCES \u{250}x(a)";
let idx = find_keyword(s, "REFERENCES").expect("keyword present");
assert!(s.is_char_boundary(idx));
assert_eq!(&s[idx..idx + "REFERENCES".len()], "REFERENCES");
assert_ne!(idx, s.to_uppercase().find("REFERENCES").unwrap());
}
#[test]
fn name_after_keyword_skips_guards() {
assert_eq!(
name_after_keyword(
"CREATE TABLE IF NOT EXISTS `t` (a INT)",
"TABLE",
&["IF NOT EXISTS"]
),
Some("t")
);
assert_eq!(
name_after_keyword("DROP TABLE if exists t;", "TABLE", &["IF EXISTS"]),
Some("t")
);
}
#[test]
fn name_after_keyword_ignores_a_guard_phrase_in_a_literal() {
assert_eq!(
name_after_keyword(
"CREATE TABLE users (note TEXT DEFAULT 'IF NOT EXISTS')",
"TABLE",
&["IF NOT EXISTS"],
),
Some("users")
);
}
#[test]
fn non_ascii_identifiers_survive() {
assert_eq!(
name_after_keyword("CREATE TABLE \u{250}x (a INT)", "TABLE", &["IF NOT EXISTS"]),
Some("\u{250}x")
);
}
}