use crate::error::{DbError, QueryError};
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Tok {
Ident(String),
Number(usize),
Star,
Comma,
Dot,
LParen,
RParen,
QMark,
Eq,
Lt,
Lte,
Gt,
Gte,
}
fn err(msg: impl Into<String>) -> DbError {
DbError::Query(QueryError {
message: msg.into(),
})
}
fn is_ident_start(c: char) -> bool {
c.is_ascii_alphabetic() | (c == '_')
}
fn is_ident_cont(c: char) -> bool {
c.is_ascii_alphanumeric() | (c == '_')
}
pub(crate) fn lex(input: &str) -> Result<Vec<Tok>, DbError> {
let mut out = Vec::new();
let mut chars = input.chars().peekable();
while let Some(c) = chars.peek().copied() {
if c.is_whitespace() {
chars.next();
continue;
}
match c {
_ if c.is_ascii_digit() => {
let mut s = String::new();
while let Some(n) = chars.peek().copied() {
if n.is_ascii_digit() {
s.push(chars.next().unwrap());
} else {
break;
}
}
let n = s
.parse::<usize>()
.map_err(|_| err("invalid integer literal"))?;
out.push(Tok::Number(n));
}
'*' => {
chars.next();
out.push(Tok::Star);
}
',' => {
chars.next();
out.push(Tok::Comma);
}
'.' => {
chars.next();
out.push(Tok::Dot);
}
'(' => {
chars.next();
out.push(Tok::LParen);
}
')' => {
chars.next();
out.push(Tok::RParen);
}
'?' => {
chars.next();
out.push(Tok::QMark);
}
'=' => {
chars.next();
out.push(Tok::Eq);
}
'<' => {
chars.next();
if chars.peek().copied() == Some('=') {
chars.next();
out.push(Tok::Lte);
} else {
out.push(Tok::Lt);
}
}
'>' => {
chars.next();
if chars.peek().copied() == Some('=') {
chars.next();
out.push(Tok::Gte);
} else {
out.push(Tok::Gt);
}
}
_ if is_ident_start(c) => {
let mut s = String::new();
s.push(chars.next().unwrap());
while let Some(n) = chars.peek().copied() {
if is_ident_cont(n) {
s.push(chars.next().unwrap());
} else {
break;
}
}
out.push(Tok::Ident(s));
}
_ => return Err(err(format!("unsupported character in SQL: {c:?}"))),
}
}
Ok(out)
}
pub(crate) fn ident_eq(a: &str, b: &str) -> bool {
a.eq_ignore_ascii_case(b)
}