#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Dialect {
Nql,
Sql,
}
impl Dialect {
pub fn name(self) -> &'static str {
match self {
Dialect::Nql => "nql",
Dialect::Sql => "sql",
}
}
}
pub const NQL_HEADS: &[&str] = &["FROM"];
pub const SQL_HEADS: &[&str] = &[
"SELECT", "INSERT", "UPDATE", "DELETE", "EXPLAIN", "WITH", "SHOW", "SET",
"VALUES", "TABLE", "BEGIN", "COMMIT", "ROLLBACK",
];
fn first_word(s: &str) -> Option<String> {
s.split_whitespace()
.next()
.map(|w| w.trim_start_matches('(').trim_end_matches(';').to_uppercase())
.filter(|w| !w.is_empty())
}
pub fn route(q: &str) -> Result<Dialect, String> {
let Some(head) = first_word(q) else {
return Err("the statement is empty".to_string());
};
if NQL_HEADS.contains(&head.as_str()) {
return Ok(Dialect::Nql);
}
if SQL_HEADS.contains(&head.as_str()) {
return Ok(Dialect::Sql);
}
Err(format!(
"{:?} does not begin a statement in either half of neQL\n \
NQL statements begin with: {}\n \
SQL statements begin with: {}",
head,
NQL_HEADS.join(", "),
SQL_HEADS.join(", "),
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_two_vocabularies_do_not_overlap() {
for n in NQL_HEADS {
assert!(
!SQL_HEADS.contains(n),
"{:?} begins a statement in both dialects — routing is no longer structural",
n
);
}
}
#[test]
fn each_dialect_routes_to_itself() {
assert_eq!(route("FROM orders").unwrap(), Dialect::Nql);
assert_eq!(route("from orders WHERE x = 1").unwrap(), Dialect::Nql);
assert_eq!(route("SELECT * FROM orders").unwrap(), Dialect::Sql);
assert_eq!(route(" explain select 1").unwrap(), Dialect::Sql);
assert_eq!(route("(SELECT 1) UNION (SELECT 2)").unwrap(), Dialect::Sql);
assert_eq!(route("INSERT INTO o VALUES (1)").unwrap(), Dialect::Sql);
}
#[test]
fn a_word_in_neither_vocabulary_is_refused_naming_both() {
let e = route("GRANT ALL ON orders").unwrap_err();
assert!(e.contains("GRANT"), "{}", e);
assert!(e.contains("FROM"), "the refusal must name the NQL vocabulary: {}", e);
assert!(e.contains("SELECT"), "and the SQL one: {}", e);
}
#[test]
fn an_empty_statement_is_refused_rather_than_routed() {
assert!(route("").is_err());
assert!(route(" \n ").is_err());
}
#[test]
fn a_sql_statement_is_never_handed_to_the_nql_parser() {
assert_eq!(route("SELECT who FROM orders").unwrap(), Dialect::Sql);
assert_eq!(route("FROM orders").unwrap(), Dialect::Nql);
}
}