#[cfg(test)]
mod tests;
use wasm_dbms_api::prelude::SqlError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Position {
pub(crate) line: usize,
pub(crate) col: usize,
}
impl Position {
pub(crate) fn error(self, msg: impl Into<String>) -> SqlError {
SqlError::Parse {
line: self.line,
col: self.col,
msg: msg.into(),
}
}
}
macro_rules! keywords {
($($variant:ident => $text:literal),+ $(,)?) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Keyword {
$($variant),+
}
impl Keyword {
pub(crate) const ALL: &'static [Keyword] = &[$(Keyword::$variant),+];
pub(crate) fn as_str(self) -> &'static str {
match self {
$(Keyword::$variant => $text),+
}
}
pub(crate) fn from_word(word: &str) -> Option<Self> {
Self::ALL
.iter()
.copied()
.find(|keyword| keyword.as_str().eq_ignore_ascii_case(word))
}
}
};
}
keywords! {
And => "AND",
As => "AS",
Asc => "ASC",
Begin => "BEGIN",
By => "BY",
Cascade => "CASCADE",
Commit => "COMMIT",
Cross => "CROSS",
Delete => "DELETE",
Desc => "DESC",
Distinct => "DISTINCT",
Except => "EXCEPT",
False => "FALSE",
From => "FROM",
Full => "FULL",
Group => "GROUP",
Having => "HAVING",
In => "IN",
Inner => "INNER",
Insert => "INSERT",
Intersect => "INTERSECT",
Into => "INTO",
Is => "IS",
Join => "JOIN",
Left => "LEFT",
Like => "LIKE",
Limit => "LIMIT",
Natural => "NATURAL",
Not => "NOT",
Null => "NULL",
Offset => "OFFSET",
On => "ON",
Or => "OR",
Order => "ORDER",
Outer => "OUTER",
Restrict => "RESTRICT",
Right => "RIGHT",
Rollback => "ROLLBACK",
Select => "SELECT",
Set => "SET",
Transaction => "TRANSACTION",
True => "TRUE",
Union => "UNION",
Update => "UPDATE",
Using => "USING",
Values => "VALUES",
Where => "WHERE",
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Token {
Keyword(Keyword),
Identifier(String),
Integer(u64),
Float(String),
String(String),
Placeholder,
Comma,
Dot,
LeftParen,
RightParen,
Star,
Semicolon,
Minus,
Eq,
NotEq,
Lt,
LtEq,
Gt,
GtEq,
Eof,
}
impl Token {
pub(crate) fn describe(&self) -> String {
match self {
Token::Keyword(keyword) => format!("keyword `{keyword}`", keyword = keyword.as_str()),
Token::Identifier(name) => format!("identifier `{name}`"),
Token::Integer(value) => format!("number `{value}`"),
Token::Float(value) => format!("number `{value}`"),
Token::String(_) => "string literal".to_string(),
Token::Placeholder => "`?`".to_string(),
Token::Comma => "`,`".to_string(),
Token::Dot => "`.`".to_string(),
Token::LeftParen => "`(`".to_string(),
Token::RightParen => "`)`".to_string(),
Token::Star => "`*`".to_string(),
Token::Semicolon => "`;`".to_string(),
Token::Minus => "`-`".to_string(),
Token::Eq => "`=`".to_string(),
Token::NotEq => "`!=`".to_string(),
Token::Lt => "`<`".to_string(),
Token::LtEq => "`<=`".to_string(),
Token::Gt => "`>`".to_string(),
Token::GtEq => "`>=`".to_string(),
Token::Eof => "end of input".to_string(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Spanned {
pub(crate) token: Token,
pub(crate) position: Position,
}
pub(crate) fn tokenize(sql: &str) -> Result<Vec<Spanned>, SqlError> {
let mut lexer = Lexer::new(sql);
let mut tokens = Vec::new();
loop {
lexer.skip_trivia()?;
let position = lexer.position;
let token = lexer.next_token()?;
let done = token == Token::Eof;
tokens.push(Spanned { token, position });
if done {
return Ok(tokens);
}
}
}
struct Lexer {
chars: Vec<char>,
index: usize,
position: Position,
}
impl Lexer {
fn new(sql: &str) -> Self {
Self {
chars: sql.chars().collect(),
index: 0,
position: Position { line: 1, col: 1 },
}
}
fn peek_at(&self, offset: usize) -> Option<char> {
self.chars.get(self.index + offset).copied()
}
fn peek(&self) -> Option<char> {
self.peek_at(0)
}
fn bump(&mut self) -> Option<char> {
let c = self.peek()?;
self.index += 1;
if c == '\n' {
self.position.line += 1;
self.position.col = 1;
} else {
self.position.col += 1;
}
Some(c)
}
fn single(&mut self, token: Token) -> Token {
self.bump();
token
}
fn double(&mut self, token: Token) -> Token {
self.bump();
self.bump();
token
}
fn skip_trivia(&mut self) -> Result<(), SqlError> {
loop {
match (self.peek(), self.peek_at(1)) {
(Some(c), _) if c.is_whitespace() => {
self.bump();
}
(Some('-'), Some('-')) => {
while self.peek().is_some_and(|c| c != '\n') {
self.bump();
}
}
(Some('/'), Some('*')) => self.skip_block_comment()?,
_ => return Ok(()),
}
}
}
fn skip_block_comment(&mut self) -> Result<(), SqlError> {
let start = self.position;
self.bump();
self.bump();
loop {
match (self.peek(), self.peek_at(1)) {
(Some('*'), Some('/')) => {
self.bump();
self.bump();
return Ok(());
}
(Some(_), _) => {
self.bump();
}
(None, _) => return Err(start.error("unterminated block comment")),
}
}
}
fn next_token(&mut self) -> Result<Token, SqlError> {
let position = self.position;
let Some(c) = self.peek() else {
return Ok(Token::Eof);
};
let token = match (c, self.peek_at(1)) {
(',', _) => self.single(Token::Comma),
('.', _) => self.single(Token::Dot),
('(', _) => self.single(Token::LeftParen),
(')', _) => self.single(Token::RightParen),
('*', _) => self.single(Token::Star),
(';', _) => self.single(Token::Semicolon),
('-', _) => self.single(Token::Minus),
('?', _) => self.single(Token::Placeholder),
('=', _) => self.single(Token::Eq),
('!', Some('=')) | ('<', Some('>')) => self.double(Token::NotEq),
('<', Some('=')) => self.double(Token::LtEq),
('<', _) => self.single(Token::Lt),
('>', Some('=')) => self.double(Token::GtEq),
('>', _) => self.single(Token::Gt),
('\'', _) => Token::String(self.quoted('\'', "unterminated string literal")?),
('"', _) => {
let name = self.quoted('"', "unterminated quoted identifier")?;
if name.is_empty() {
return Err(position.error("quoted identifier cannot be empty"));
}
Token::Identifier(name)
}
('`', _) => {
return Err(position
.error("backtick-quoted identifiers are not supported; use double quotes"));
}
(':' | '$' | '@', _) => {
return Err(
position.error("named and numbered placeholders are not supported; use `?`")
);
}
(c, _) if c.is_ascii_digit() => self.number()?,
(c, _) if c.is_ascii_alphabetic() || c == '_' => self.word(),
(c, _) => return Err(position.error(format!("unexpected character `{c}`"))),
};
Ok(token)
}
fn quoted(&mut self, quote: char, unterminated: &str) -> Result<String, SqlError> {
let start = self.position;
self.bump();
let mut content = String::new();
loop {
match (self.bump(), self.peek()) {
(Some(c), Some(next)) if c == quote && next == quote => {
self.bump();
content.push(quote);
}
(Some(c), _) if c == quote => return Ok(content),
(Some(c), _) => content.push(c),
(None, _) => return Err(start.error(unterminated)),
}
}
}
fn number(&mut self) -> Result<Token, SqlError> {
let start = self.position;
let mut text = self.digits();
let is_float =
self.peek() == Some('.') && self.peek_at(1).is_some_and(|c| c.is_ascii_digit());
if is_float {
self.bump();
text.push('.');
text.push_str(&self.digits());
}
if self
.peek()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
{
return Err(start.error("invalid numeric literal"));
}
if is_float {
return Ok(Token::Float(text));
}
text.parse()
.map(Token::Integer)
.map_err(|_| start.error("integer literal is too large"))
}
fn digits(&mut self) -> String {
let mut digits = String::new();
while let Some(c) = self.peek().filter(char::is_ascii_digit) {
self.bump();
digits.push(c);
}
digits
}
fn word(&mut self) -> Token {
let mut word = String::new();
while let Some(c) = self
.peek()
.filter(|c| c.is_ascii_alphanumeric() || *c == '_')
{
self.bump();
word.push(c);
}
match Keyword::from_word(&word) {
Some(keyword) => Token::Keyword(keyword),
None => Token::Identifier(word),
}
}
}