use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::fmt;
#[derive(Debug, Clone, PartialEq)]
pub enum Token {
Select,
From,
Where,
As,
Null,
True,
False,
And,
Or,
Not,
Create,
Table,
Insert,
Into,
Values,
Index,
On,
Begin,
Commit,
Rollback,
Order,
By,
Limit,
Ident(String), QuotedIdent(String), SessionVar(String),
Integer(i64),
Float(f64),
Numeric(String),
String(String),
HexBytes(String),
Plus,
Minus,
Star,
Slash,
Percent,
Eq,
NotEq,
Lt,
LtEq,
Gt,
GtEq,
InetContainedBy,
InetContainedByEq,
InetContains,
InetContainsEq,
InetOverlap,
OverLeft,
OverRight,
LParen,
RParen,
LBracket,
RBracket,
Comma,
Semicolon,
Dot,
DotDot,
Bang,
At,
JsonGet,
JsonGetText,
JsonGetPath,
JsonGetPathText,
JsonDeletePath,
JsonContains,
JsonPathExists,
JsonContainedBy,
JsonKeyExists,
JsonKeysAny,
GeomParallel,
GeomPerp,
GeomSameAs,
ClosestPoint,
GeomHoriz,
JsonKeysAll,
TsMatch,
TsMatchOld,
AtMinusAt,
Intersects,
IsBelow,
IsAbove,
PatternLt,
PatternLtEq,
PatternGt,
PatternGtEq,
L2Distance,
InnerProduct,
CosineDistance,
DoubleColon,
ColonEq,
FatArrow,
Colon,
Concat,
Pipe,
Amp,
Tilde,
TildeStar,
NotTilde,
NotTildeStar,
DoubleTilde,
DoubleTildeStar,
NotDoubleTilde,
NotDoubleTildeStar,
Caret,
CaretAt,
Hash,
Adjacent,
DoubleBang,
Is,
Between,
In,
Like,
Group,
Distinct,
Union,
All,
Join,
Inner,
Left,
Cross,
Outer,
Right,
Full,
Default,
Savepoint,
Release,
To,
Having,
Show,
Extract,
Offset,
Asc,
Desc,
Interval,
Placeholder(u16),
Drop,
For,
Tables,
Except,
Publication,
Subscription,
Connection,
Partition,
Eof,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LexErrorKind {
InvalidByteSequence(u8),
UnknownChar(char),
UnterminatedString,
UnterminatedQuotedIdent,
UnterminatedBlockComment,
BadNumber(String),
TrailingJunkAfterNumber(String),
InvalidRadixLiteral(&'static str, String),
InvalidUnicodeEscape,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LexError {
pub kind: LexErrorKind,
pub pos: usize,
}
impl fmt::Display for LexError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.kind {
LexErrorKind::InvalidByteSequence(b) => {
write!(f, "invalid byte sequence for encoding \"UTF8\": 0x{b:02x}")
}
LexErrorKind::UnknownChar(c) => write!(f, "unknown char {c:?} at byte {}", self.pos),
LexErrorKind::UnterminatedString => {
write!(f, "unterminated string literal at byte {}", self.pos)
}
LexErrorKind::UnterminatedQuotedIdent => {
write!(f, "unterminated quoted identifier at byte {}", self.pos)
}
LexErrorKind::UnterminatedBlockComment => {
write!(f, "unterminated /* */ comment at byte {}", self.pos)
}
LexErrorKind::BadNumber(s) => {
write!(f, "invalid number literal {s:?} at byte {}", self.pos)
}
LexErrorKind::TrailingJunkAfterNumber(s) => {
write!(f, "trailing junk after numeric literal at or near \"{s}\"")
}
LexErrorKind::InvalidRadixLiteral(radix, s) => {
write!(f, "invalid {radix} integer at or near \"{s}\"")
}
LexErrorKind::InvalidUnicodeEscape => {
write!(f, "invalid Unicode escape at byte {}", self.pos)
}
}
}
}
pub fn tokenize(input: &str) -> Result<Vec<Token>, LexError> {
tokenize_with(input, false)
}
pub fn tokenize_with(input: &str, backslash_escapes: bool) -> Result<Vec<Token>, LexError> {
tokenize_with_offsets(input, backslash_escapes).map(|(tokens, _)| tokens)
}
#[allow(clippy::too_many_lines)] pub fn tokenize_with_offsets(
input: &str,
backslash_escapes: bool,
) -> Result<(Vec<Token>, Vec<usize>), LexError> {
let bytes = input.as_bytes();
let mut i = 0usize;
let mut out = Vec::new();
let mut offsets: Vec<usize> = Vec::new();
while i < bytes.len() {
let start = i;
let b = bytes[i];
match b {
b' ' | b'\t' | b'\n' | b'\r' => {
i += 1;
}
b'-' if peek_eq(bytes, i + 1, b'-') => {
i += 2;
while i < bytes.len() && bytes[i] != b'\n' {
i += 1;
}
}
b'/' if peek_eq(bytes, i + 1, b'*') => {
let start = i;
if peek_eq(bytes, i + 2, b'!') {
let mut j = i + 3;
while j < bytes.len() && bytes[j].is_ascii_digit() {
j += 1;
}
if j < bytes.len() && (bytes[j] == b' ' || bytes[j] == b'\t') {
j += 1;
}
i = j;
continue;
}
i += 2;
let mut closed = false;
while i + 1 < bytes.len() {
if bytes[i] == b'*' && bytes[i + 1] == b'/' {
i += 2;
closed = true;
break;
}
i += 1;
}
if !closed {
return Err(LexError {
kind: LexErrorKind::UnterminatedBlockComment,
pos: start,
});
}
}
b'*' if peek_eq(bytes, i + 1, b'/') => {
i += 2;
}
b'\'' => {
let (tok, consumed) = if backslash_escapes {
lex_escape_string(input, i, true)?
} else {
lex_quoted(input, i, b'\'', false)?
};
out.push(tok);
i += consumed;
}
b'E' | b'e' if peek_eq(bytes, i + 1, b'\'') => {
let (tok, consumed) = lex_escape_string(input, i + 1, false)?;
out.push(tok);
i += 1 + consumed;
}
b'U' | b'u' if peek_eq(bytes, i + 1, b'&') && peek_eq(bytes, i + 2, b'\'') => {
let (tok, consumed) = lex_unicode_string(input, i + 2)?;
out.push(tok);
i += 2 + consumed;
}
b'"' => {
let (tok, consumed) = lex_quoted(input, i, b'"', true)?;
out.push(tok);
i += consumed;
}
b'`' => {
let (tok, consumed) = lex_quoted(input, i, b'`', true)?;
out.push(tok);
i += consumed;
}
b if b.is_ascii_alphabetic() || b == b'_' => {
let start = i;
i += 1;
while i < bytes.len() {
let c = bytes[i];
if c.is_ascii_alphanumeric() || c == b'_' {
i += 1;
} else {
break;
}
}
let raw = &input[start..i];
out.push(keyword_or_ident_raw(raw));
}
b if b.is_ascii_digit() => {
let (tok, consumed) = lex_number(&input[i..], backslash_escapes)
.map_err(|kind| LexError { kind, pos: i })?;
out.push(tok);
i += consumed;
}
b'.' if peek_pred(bytes, i + 1, u8::is_ascii_digit) => {
let (tok, consumed) = lex_number(&input[i..], backslash_escapes)
.map_err(|kind| LexError { kind, pos: i })?;
out.push(tok);
i += consumed;
}
b'+' => single(&mut out, Token::Plus, &mut i),
b'?' if peek_eq(bytes, i + 1, b'|') && peek_eq(bytes, i + 2, b'|') => {
out.push(Token::GeomParallel);
i += 3;
}
b'?' if peek_eq(bytes, i + 1, b'-') && peek_eq(bytes, i + 2, b'|') => {
out.push(Token::GeomPerp);
i += 3;
}
b'?' if peek_eq(bytes, i + 1, b'|') => {
out.push(Token::JsonKeysAny);
i += 2;
}
b'?' if peek_eq(bytes, i + 1, b'&') => {
out.push(Token::JsonKeysAll);
i += 2;
}
b'?' if peek_eq(bytes, i + 1, b'-') => {
out.push(Token::GeomHoriz);
i += 2;
}
b'?' if peek_eq(bytes, i + 1, b'#') => {
out.push(Token::Intersects);
i += 2;
}
b'?' => single(&mut out, Token::JsonKeyExists, &mut i),
b'-' => {
if peek_eq(bytes, i + 1, b'|') && peek_eq(bytes, i + 2, b'-') {
out.push(Token::Adjacent);
i += 3;
}
else if peek_eq(bytes, i + 1, b'>') && peek_eq(bytes, i + 2, b'>') {
out.push(Token::JsonGetText);
i += 3;
} else if peek_eq(bytes, i + 1, b'>') {
out.push(Token::JsonGet);
i += 2;
} else {
single(&mut out, Token::Minus, &mut i);
}
}
b'#' => {
if peek_eq(bytes, i + 1, b'>') && peek_eq(bytes, i + 2, b'>') {
out.push(Token::JsonGetPathText);
i += 3;
} else if peek_eq(bytes, i + 1, b'#') {
out.push(Token::ClosestPoint);
i += 2;
} else if peek_eq(bytes, i + 1, b'>') {
out.push(Token::JsonGetPath);
i += 2;
} else if peek_eq(bytes, i + 1, b'-') {
out.push(Token::JsonDeletePath);
i += 2;
} else {
single(&mut out, Token::Hash, &mut i);
}
}
b'@' => {
if peek_eq(bytes, i + 1, b'>') {
out.push(Token::JsonContains);
i += 2;
} else if peek_eq(bytes, i + 1, b'?') {
out.push(Token::JsonPathExists);
i += 2;
} else if peek_eq(bytes, i + 1, b'@') && peek_eq(bytes, i + 2, b'@') {
out.push(Token::TsMatchOld);
i += 3;
} else if peek_eq(bytes, i + 1, b'-') && peek_eq(bytes, i + 2, b'@') {
out.push(Token::AtMinusAt);
i += 3;
} else if peek_eq(bytes, i + 1, b'@')
&& !is_session_var_ident_start(bytes.get(i + 2).copied())
{
out.push(Token::TsMatch);
i += 2;
} else {
let prefix_end = if peek_eq(bytes, i + 1, b'@') {
i + 2
} else {
i + 1
};
let mut end = prefix_end;
while end < bytes.len() && is_session_var_ident_continue(bytes[end]) {
end += 1;
}
if end == prefix_end {
out.push(Token::At);
i = prefix_end;
} else {
out.push(Token::SessionVar(input[i..end].to_string()));
i = end;
}
}
}
b'*' => single(&mut out, Token::Star, &mut i),
b'/' => single(&mut out, Token::Slash, &mut i),
b'%' => single(&mut out, Token::Percent, &mut i),
b'(' => single(&mut out, Token::LParen, &mut i),
b')' => single(&mut out, Token::RParen, &mut i),
b'[' => single(&mut out, Token::LBracket, &mut i),
b']' => single(&mut out, Token::RBracket, &mut i),
b',' => single(&mut out, Token::Comma, &mut i),
b';' => single(&mut out, Token::Semicolon, &mut i),
b'.' => {
if peek_eq(bytes, i + 1, b'.') {
out.push(Token::DotDot);
i += 2;
} else {
single(&mut out, Token::Dot, &mut i);
}
}
b'=' => {
if peek_eq(bytes, i + 1, b'>') {
out.push(Token::FatArrow);
i += 2;
} else {
single(&mut out, Token::Eq, &mut i);
}
}
b'<' => {
if peek_eq(bytes, i + 1, b'=') && peek_eq(bytes, i + 2, b'>') {
out.push(Token::CosineDistance);
i += 3;
} else if peek_eq(bytes, i + 1, b'#') && peek_eq(bytes, i + 2, b'>') {
out.push(Token::InnerProduct);
i += 3;
} else if peek_eq(bytes, i + 1, b'-') && peek_eq(bytes, i + 2, b'>') {
out.push(Token::L2Distance);
i += 3;
} else if peek_eq(bytes, i + 1, b'<') && peek_eq(bytes, i + 2, b'=') {
out.push(Token::InetContainedByEq);
i += 3;
} else if peek_eq(bytes, i + 1, b'<') {
out.push(Token::InetContainedBy);
i += 2;
} else if peek_eq(bytes, i + 1, b'^') {
out.push(Token::IsBelow);
i += 2;
} else if peek_eq(bytes, i + 1, b'@') {
out.push(Token::JsonContainedBy);
i += 2;
} else if peek_eq(bytes, i + 1, b'=') {
out.push(Token::LtEq);
i += 2;
} else if peek_eq(bytes, i + 1, b'>') {
out.push(Token::NotEq);
i += 2;
} else {
out.push(Token::Lt);
i += 1;
}
}
b':' if peek_eq(bytes, i + 1, b':') => {
out.push(Token::DoubleColon);
i += 2;
}
b':' if peek_eq(bytes, i + 1, b'=') => {
out.push(Token::ColonEq);
i += 2;
}
b':' => {
out.push(Token::Colon);
i += 1;
}
b'|' if peek_eq(bytes, i + 1, b'|') => {
out.push(Token::Concat);
i += 2;
}
b'|' => {
single(&mut out, Token::Pipe, &mut i);
}
b'~' if peek_eq(bytes, i + 1, b'~') && peek_eq(bytes, i + 2, b'*') => {
out.push(Token::DoubleTildeStar);
i += 3;
}
b'~' if peek_eq(bytes, i + 1, b'~') => {
out.push(Token::DoubleTilde);
i += 2;
}
b'~' if peek_eq(bytes, i + 1, b'*') => {
out.push(Token::TildeStar);
i += 2;
}
b'~' if peek_eq(bytes, i + 1, b'=') => {
out.push(Token::GeomSameAs);
i += 2;
}
b'~' if peek_eq(bytes, i + 1, b'<')
&& peek_eq(bytes, i + 2, b'=')
&& peek_eq(bytes, i + 3, b'~') =>
{
out.push(Token::PatternLtEq);
i += 4;
}
b'~' if peek_eq(bytes, i + 1, b'>')
&& peek_eq(bytes, i + 2, b'=')
&& peek_eq(bytes, i + 3, b'~') =>
{
out.push(Token::PatternGtEq);
i += 4;
}
b'~' if peek_eq(bytes, i + 1, b'<') && peek_eq(bytes, i + 2, b'~') => {
out.push(Token::PatternLt);
i += 3;
}
b'~' if peek_eq(bytes, i + 1, b'>') && peek_eq(bytes, i + 2, b'~') => {
out.push(Token::PatternGt);
i += 3;
}
b'~' => {
single(&mut out, Token::Tilde, &mut i);
}
b'^' if peek_eq(bytes, i + 1, b'@') => {
out.push(Token::CaretAt);
i += 2;
}
b'^' => {
single(&mut out, Token::Caret, &mut i);
}
b'>' => {
if peek_eq(bytes, i + 1, b'^') {
out.push(Token::IsAbove);
i += 2;
} else if peek_eq(bytes, i + 1, b'>') && peek_eq(bytes, i + 2, b'=') {
out.push(Token::InetContainsEq);
i += 3;
} else if peek_eq(bytes, i + 1, b'>') {
out.push(Token::InetContains);
i += 2;
} else if peek_eq(bytes, i + 1, b'=') {
out.push(Token::GtEq);
i += 2;
} else {
out.push(Token::Gt);
i += 1;
}
}
b'&' if peek_eq(bytes, i + 1, b'&') => {
out.push(Token::InetOverlap);
i += 2;
}
b'&' if peek_eq(bytes, i + 1, b'<') => {
out.push(Token::OverLeft);
i += 2;
}
b'&' if peek_eq(bytes, i + 1, b'>') => {
out.push(Token::OverRight);
i += 2;
}
b'&' => {
single(&mut out, Token::Amp, &mut i);
}
b'!' if peek_eq(bytes, i + 1, b'!') => {
out.push(Token::DoubleBang);
i += 2;
}
b'!' if peek_eq(bytes, i + 1, b'=') => {
out.push(Token::NotEq);
i += 2;
}
b'!' if peek_eq(bytes, i + 1, b'~')
&& peek_eq(bytes, i + 2, b'~')
&& peek_eq(bytes, i + 3, b'*') =>
{
out.push(Token::NotDoubleTildeStar);
i += 4;
}
b'!' if peek_eq(bytes, i + 1, b'~') && peek_eq(bytes, i + 2, b'~') => {
out.push(Token::NotDoubleTilde);
i += 3;
}
b'!' if peek_eq(bytes, i + 1, b'~') && peek_eq(bytes, i + 2, b'*') => {
out.push(Token::NotTildeStar);
i += 3;
}
b'!' if peek_eq(bytes, i + 1, b'~') => {
out.push(Token::NotTilde);
i += 2;
}
b'!' => {
out.push(Token::Bang);
i += 1;
}
b'$' if i + 1 < bytes.len() && bytes[i + 1] == b'$' => {
let end = find_dollar_tag_end(bytes, i + 2, b"$$");
let body = match end {
Some(e) => &input[i + 2..e],
None => {
return Err(LexError {
kind: LexErrorKind::UnterminatedString,
pos: i,
});
}
};
out.push(Token::String(body.to_string()));
i = end.unwrap() + 2;
}
b'$' if i + 1 < bytes.len()
&& (bytes[i + 1].is_ascii_alphabetic() || bytes[i + 1] == b'_') =>
{
let mut j = i + 1;
while j < bytes.len() && (bytes[j].is_ascii_alphanumeric() || bytes[j] == b'_') {
j += 1;
}
if j >= bytes.len() || bytes[j] != b'$' {
let ch = input[i..].chars().next().unwrap_or('?');
return Err(LexError {
kind: LexErrorKind::UnknownChar(ch),
pos: i,
});
}
let close: alloc::vec::Vec<u8> = bytes[i..=j].to_vec();
let end = find_dollar_tag_end(bytes, j + 1, &close);
let body = match end {
Some(e) => &input[j + 1..e],
None => {
return Err(LexError {
kind: LexErrorKind::UnterminatedString,
pos: i,
});
}
};
out.push(Token::String(body.to_string()));
i = end.unwrap() + close.len();
}
b'$' if i + 1 < bytes.len() && bytes[i + 1].is_ascii_digit() => {
let mut j = i + 1;
let mut n: u32 = 0;
while j < bytes.len() && bytes[j].is_ascii_digit() {
n = n
.saturating_mul(10)
.saturating_add(u32::from(bytes[j] - b'0'));
j += 1;
}
if n == 0 || n > u32::from(u16::MAX) {
return Err(LexError {
kind: LexErrorKind::BadNumber(input[i..j].to_string()),
pos: i,
});
}
#[allow(clippy::cast_possible_truncation)]
out.push(Token::Placeholder(n as u16));
i = j;
}
_ => {
let ch = input[i..].chars().next().unwrap_or('?');
return Err(LexError {
kind: LexErrorKind::UnknownChar(ch),
pos: i,
});
}
}
while offsets.len() < out.len() {
offsets.push(start);
}
}
out.push(Token::Eof);
offsets.push(bytes.len());
Ok((out, offsets))
}
fn peek_eq(bytes: &[u8], i: usize, target: u8) -> bool {
bytes.get(i) == Some(&target)
}
fn is_session_var_ident_start(b: Option<u8>) -> bool {
matches!(b, Some(c) if c.is_ascii_alphabetic() || c == b'_')
}
fn is_session_var_ident_continue(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_' || b == b'.' || b == b'$'
}
fn find_dollar_tag_end(bytes: &[u8], from: usize, tag: &[u8]) -> Option<usize> {
if tag.is_empty() || from > bytes.len() {
return None;
}
let mut i = from;
while i + tag.len() <= bytes.len() {
if &bytes[i..i + tag.len()] == tag {
return Some(i);
}
i += 1;
}
None
}
fn peek_pred<F: Fn(&u8) -> bool>(bytes: &[u8], i: usize, pred: F) -> bool {
bytes.get(i).is_some_and(pred)
}
fn single(out: &mut Vec<Token>, tok: Token, i: &mut usize) {
out.push(tok);
*i += 1;
}
fn keyword_or_ident_raw(raw: &str) -> Token {
let b = raw.as_bytes();
let tok = match b.len() {
2 => kw_len2(b),
3 => kw_len3(b),
4 => kw_len4(b),
5 => kw_len5(b),
6 => kw_len6(b),
7 => kw_len7(b),
8 => kw_len8(b),
9 => kw_len9(b),
10 => kw_len10(b),
11 => kw_len11(b),
12 => kw_len12(b),
_ => None,
};
match tok {
Some(t) => t,
None => Token::Ident(raw.to_ascii_lowercase()),
}
}
#[inline]
fn eq_ci(input: &[u8], lower: &[u8]) -> bool {
if input.len() != lower.len() {
return false;
}
for i in 0..lower.len() {
if input[i].to_ascii_lowercase() != lower[i] {
return false;
}
}
true
}
#[inline]
fn kw_len2(b: &[u8]) -> Option<Token> {
if eq_ci(b, b"as") {
return Some(Token::As);
}
if eq_ci(b, b"in") {
return Some(Token::In);
}
if eq_ci(b, b"is") {
return Some(Token::Is);
}
if eq_ci(b, b"on") {
return Some(Token::On);
}
if eq_ci(b, b"or") {
return Some(Token::Or);
}
if eq_ci(b, b"to") {
return Some(Token::To);
}
None
}
#[inline]
fn kw_len3(b: &[u8]) -> Option<Token> {
if eq_ci(b, b"for") {
return Some(Token::For);
}
if eq_ci(b, b"all") {
return Some(Token::All);
}
if eq_ci(b, b"and") {
return Some(Token::And);
}
if eq_ci(b, b"asc") {
return Some(Token::Asc);
}
if eq_ci(b, b"not") {
return Some(Token::Not);
}
None
}
#[inline]
fn kw_len4(b: &[u8]) -> Option<Token> {
if eq_ci(b, b"from") {
return Some(Token::From);
}
if eq_ci(b, b"drop") {
return Some(Token::Drop);
}
if eq_ci(b, b"null") {
return Some(Token::Null);
}
if eq_ci(b, b"full") {
return Some(Token::Full);
}
if eq_ci(b, b"true") {
return Some(Token::True);
}
if eq_ci(b, b"into") {
return Some(Token::Into);
}
if eq_ci(b, b"like") {
return Some(Token::Like);
}
if eq_ci(b, b"join") {
return Some(Token::Join);
}
if eq_ci(b, b"left") {
return Some(Token::Left);
}
if eq_ci(b, b"show") {
return Some(Token::Show);
}
if eq_ci(b, b"desc") {
return Some(Token::Desc);
}
None
}
#[inline]
fn kw_len5(b: &[u8]) -> Option<Token> {
if eq_ci(b, b"false") {
return Some(Token::False);
}
if eq_ci(b, b"where") {
return Some(Token::Where);
}
if eq_ci(b, b"table") {
return Some(Token::Table);
}
if eq_ci(b, b"index") {
return Some(Token::Index);
}
if eq_ci(b, b"begin") {
return Some(Token::Begin);
}
if eq_ci(b, b"order") {
return Some(Token::Order);
}
if eq_ci(b, b"limit") {
return Some(Token::Limit);
}
if eq_ci(b, b"group") {
return Some(Token::Group);
}
if eq_ci(b, b"union") {
return Some(Token::Union);
}
if eq_ci(b, b"inner") {
return Some(Token::Inner);
}
if eq_ci(b, b"cross") {
return Some(Token::Cross);
}
if eq_ci(b, b"outer") {
return Some(Token::Outer);
}
if eq_ci(b, b"right") {
return Some(Token::Right);
}
None
}
#[inline]
fn kw_len6(b: &[u8]) -> Option<Token> {
if eq_ci(b, b"select") {
return Some(Token::Select);
}
if eq_ci(b, b"tables") {
return Some(Token::Tables);
}
if eq_ci(b, b"except") {
return Some(Token::Except);
}
if eq_ci(b, b"create") {
return Some(Token::Create);
}
if eq_ci(b, b"insert") {
return Some(Token::Insert);
}
if eq_ci(b, b"values") {
return Some(Token::Values);
}
if eq_ci(b, b"commit") {
return Some(Token::Commit);
}
if eq_ci(b, b"having") {
return Some(Token::Having);
}
if eq_ci(b, b"offset") {
return Some(Token::Offset);
}
None
}
#[inline]
fn kw_len7(b: &[u8]) -> Option<Token> {
if eq_ci(b, b"between") {
return Some(Token::Between);
}
if eq_ci(b, b"default") {
return Some(Token::Default);
}
if eq_ci(b, b"release") {
return Some(Token::Release);
}
if eq_ci(b, b"extract") {
return Some(Token::Extract);
}
None
}
#[inline]
fn kw_len8(b: &[u8]) -> Option<Token> {
if eq_ci(b, b"rollback") {
return Some(Token::Rollback);
}
if eq_ci(b, b"distinct") {
return Some(Token::Distinct);
}
if eq_ci(b, b"interval") {
return Some(Token::Interval);
}
None
}
#[inline]
fn kw_len9(b: &[u8]) -> Option<Token> {
if eq_ci(b, b"savepoint") {
return Some(Token::Savepoint);
}
if eq_ci(b, b"partition") {
return Some(Token::Partition);
}
None
}
#[inline]
fn kw_len10(b: &[u8]) -> Option<Token> {
if eq_ci(b, b"connection") {
return Some(Token::Connection);
}
None
}
#[inline]
fn kw_len11(b: &[u8]) -> Option<Token> {
if eq_ci(b, b"publication") {
return Some(Token::Publication);
}
None
}
#[inline]
fn kw_len12(b: &[u8]) -> Option<Token> {
if eq_ci(b, b"subscription") {
return Some(Token::Subscription);
}
None
}
fn lex_quoted(
input: &str,
start: usize,
quote: u8,
is_ident: bool,
) -> Result<(Token, usize), LexError> {
let bytes = input.as_bytes();
let mut i = start + 1;
let mut s = String::new();
loop {
if i >= bytes.len() {
return Err(LexError {
kind: if is_ident {
LexErrorKind::UnterminatedQuotedIdent
} else {
LexErrorKind::UnterminatedString
},
pos: start,
});
}
if bytes[i] == quote {
if peek_eq(bytes, i + 1, quote) {
s.push(quote as char);
i += 2;
} else {
i += 1;
break;
}
} else {
let ch = input[i..].chars().next().expect("non-empty UTF-8 boundary");
s.push(ch);
i += ch.len_utf8();
}
}
let tok = if is_ident {
Token::QuotedIdent(s)
} else {
Token::String(s)
};
Ok((tok, i - start))
}
fn lex_escape_string(input: &str, start: usize, mysql: bool) -> Result<(Token, usize), LexError> {
let bytes = input.as_bytes();
debug_assert_eq!(bytes[start], b'\'');
let mut i = start + 1;
let mut buf: Vec<u8> = Vec::new();
let mut push_char = |buf: &mut Vec<u8>, c: char| {
let mut tmp = [0u8; 4];
buf.extend_from_slice(c.encode_utf8(&mut tmp).as_bytes());
};
loop {
if i >= bytes.len() {
return Err(LexError {
kind: LexErrorKind::UnterminatedString,
pos: start,
});
}
let b = bytes[i];
if b == b'\'' {
if peek_eq(bytes, i + 1, b'\'') {
push_char(&mut buf, '\'');
i += 2;
continue;
}
i += 1;
break;
}
if b == b'\\' && i + 1 < bytes.len() {
let n = bytes[i + 1];
if mysql {
match n {
b'Z' => {
push_char(&mut buf, '\u{001A}');
i += 2;
continue;
}
b'%' | b'_' => {
push_char(&mut buf, '\\');
push_char(&mut buf, n as char);
i += 2;
continue;
}
b'x' | b'X' => {
push_char(&mut buf, 'x');
i += 2;
continue;
}
d if d.is_ascii_digit() && d != b'0' => {
push_char(&mut buf, d as char);
i += 2;
continue;
}
_ => {}
}
}
match n {
b'\\' => {
push_char(&mut buf, '\\');
i += 2;
}
b'\'' => {
push_char(&mut buf, '\'');
i += 2;
}
b'"' => {
push_char(&mut buf, '"');
i += 2;
}
b'n' => {
push_char(&mut buf, '\n');
i += 2;
}
b'r' => {
push_char(&mut buf, '\r');
i += 2;
}
b't' => {
push_char(&mut buf, '\t');
i += 2;
}
b'b' => {
push_char(&mut buf, '\u{0008}');
i += 2;
}
b'f' => {
push_char(&mut buf, '\u{000C}');
i += 2;
}
b'v' => {
push_char(&mut buf, '\u{000B}');
i += 2;
}
b'u' | b'U' => {
let is_u = bytes[i + 1] == b'u';
let ndigits = if is_u { 4 } else { 8 };
let Some(cp) = read_hex_run(bytes, i + 2, ndigits) else {
return Err(LexError {
kind: LexErrorKind::InvalidUnicodeEscape,
pos: i,
});
};
if is_u && (0xD800..=0xDBFF).contains(&cp) {
let lo = (bytes.get(i + 6) == Some(&b'\\')
&& bytes.get(i + 7) == Some(&b'u'))
.then(|| read_hex_run(bytes, i + 8, 4))
.flatten()
.filter(|l| (0xDC00..=0xDFFF).contains(l));
let Some(lo) = lo else {
return Err(LexError {
kind: LexErrorKind::InvalidUnicodeEscape,
pos: i,
});
};
let combined = 0x10000 + ((cp - 0xD800) << 10) + (lo - 0xDC00);
push_char(
&mut buf,
char::from_u32(combined).ok_or(LexError {
kind: LexErrorKind::InvalidUnicodeEscape,
pos: i,
})?,
);
i += 12;
} else {
push_char(
&mut buf,
char::from_u32(cp).ok_or(LexError {
kind: LexErrorKind::InvalidUnicodeEscape,
pos: i,
})?,
);
i += 2 + ndigits;
}
}
b'0' if i + 2 >= bytes.len() || !bytes[i + 2].is_ascii_digit() => {
push_char(&mut buf, '\0');
i += 2;
}
b'x' => {
let h1 = bytes.get(i + 2).copied();
let h2 = bytes.get(i + 3).copied();
let n1 = h1.and_then(hex_digit_value);
let n2 = h2.and_then(hex_digit_value);
match (n1, n2) {
(Some(a), Some(b2)) => {
buf.push(((a << 4) | b2) as u8);
i += 4;
}
(Some(a), _) => {
buf.push(a as u8);
i += 3;
}
_ => {
push_char(&mut buf, 'x');
i += 2;
}
}
}
d if d.is_ascii_digit() && d < b'8' => {
let mut value: u32 = u32::from(d - b'0');
let mut take = 2;
while take < 4 {
let next = bytes.get(i + take).copied();
match next {
Some(c) if c.is_ascii_digit() && c < b'8' => {
value = (value << 3) | u32::from(c - b'0');
take += 1;
}
_ => break,
}
}
buf.push((value & 0xFF) as u8);
i += take;
}
other => {
push_char(&mut buf, other as char);
i += 2;
}
}
} else {
let ch = input[i..].chars().next().expect("non-empty UTF-8 boundary");
push_char(&mut buf, ch);
i += ch.len_utf8();
}
}
match String::from_utf8(buf) {
Ok(decoded) => Ok((Token::String(decoded), i - start)),
Err(e) => {
let bad = e.as_bytes()[e.utf8_error().valid_up_to()];
Err(LexError {
kind: LexErrorKind::InvalidByteSequence(bad),
pos: start,
})
}
}
}
fn lex_unicode_string(input: &str, start: usize) -> Result<(Token, usize), LexError> {
let bytes = input.as_bytes();
debug_assert_eq!(bytes[start], b'\'');
let hex_char = |hex: &str, pos: usize| -> Result<char, LexError> {
u32::from_str_radix(hex, 16)
.ok()
.and_then(char::from_u32)
.ok_or(LexError {
kind: LexErrorKind::InvalidUnicodeEscape,
pos,
})
};
let mut i = start + 1;
let mut s = String::new();
loop {
if i >= bytes.len() {
return Err(LexError {
kind: LexErrorKind::UnterminatedString,
pos: start,
});
}
let b = bytes[i];
if b == b'\'' {
if peek_eq(bytes, i + 1, b'\'') {
s.push('\'');
i += 2;
continue;
}
i += 1;
break;
}
if b == b'\\' {
if peek_eq(bytes, i + 1, b'\\') {
s.push('\\');
i += 2;
continue;
}
let (lo, hi) = if peek_eq(bytes, i + 1, b'+') {
(i + 2, i + 8) } else {
(i + 1, i + 5) };
if hi > bytes.len() || !input.is_char_boundary(lo) || !input.is_char_boundary(hi) {
return Err(LexError {
kind: LexErrorKind::InvalidUnicodeEscape,
pos: i,
});
}
s.push(hex_char(&input[lo..hi], i)?);
i = hi;
continue;
}
let ch = input[i..].chars().next().expect("valid utf-8 boundary");
s.push(ch);
i += ch.len_utf8();
}
Ok((Token::String(s), i - start))
}
fn read_hex_run(bytes: &[u8], start: usize, n: usize) -> Option<u32> {
let mut v = 0u32;
for k in 0..n {
v = (v << 4) | hex_digit_value(*bytes.get(start + k)?)?;
}
Some(v)
}
fn hex_digit_value(b: u8) -> Option<u32> {
match b {
b'0'..=b'9' => Some(u32::from(b - b'0')),
b'a'..=b'f' => Some(u32::from(b - b'a' + 10)),
b'A'..=b'F' => Some(u32::from(b - b'A' + 10)),
_ => None,
}
}
fn lex_number(s: &str, mysql: bool) -> Result<(Token, usize), LexErrorKind> {
let bytes = s.as_bytes();
let mut i = 0usize;
let junk_end = |from: usize| -> usize {
let mut j = from;
while j < bytes.len() && (bytes[j] == b'_' || bytes[j].is_ascii_alphanumeric()) {
j += 1;
}
j
};
let junk_check = |end: usize| -> Result<(), LexErrorKind> {
if end < bytes.len() && (bytes[end] == b'_' || bytes[end].is_ascii_alphabetic()) {
return Err(LexErrorKind::TrailingJunkAfterNumber(
s[..junk_end(end)].to_string(),
));
}
Ok(())
};
if bytes.len() >= 2 && bytes[0] == b'0' {
let (radix, radix_name) = match bytes[1] {
b'x' | b'X' => (Some(16u32), "hexadecimal"),
b'o' | b'O' => (Some(8), "octal"),
b'b' | b'B' => (Some(2), "binary"),
_ => (None, ""),
};
if let Some(radix) = radix {
let mut j = 2;
loop {
let mut k = j;
if k < bytes.len() && bytes[k] == b'_' {
k += 1;
}
if k < bytes.len() && (bytes[k] as char).is_digit(radix) {
j = k + 1;
} else {
break;
}
}
let digits: alloc::string::String = s[2..j].chars().filter(|c| *c != '_').collect();
if digits.is_empty() {
return Err(LexErrorKind::InvalidRadixLiteral(
radix_name,
s[..junk_end(0)].to_string(),
));
}
junk_check(j)?;
if mysql && radix == 16 {
return Ok((Token::HexBytes(digits), j));
}
return match i64::from_str_radix(&digits, radix) {
Ok(v) => Ok((Token::Integer(v), j)),
Err(_) => match u128::from_str_radix(&digits, radix) {
Ok(v) => Ok((Token::Numeric(alloc::format!("{v}")), j)),
Err(_) => Err(LexErrorKind::BadNumber(s[..j].to_string())),
},
};
}
}
let mut has_dot = false;
let mut has_exp = false;
let digit_or_sep = |bytes: &[u8], i: usize| -> bool {
bytes[i].is_ascii_digit()
|| (bytes[i] == b'_' && i + 1 < bytes.len() && bytes[i + 1].is_ascii_digit())
};
while i < bytes.len() && digit_or_sep(bytes, i) {
i += 1;
}
if i < bytes.len() && bytes[i] == b'.' && !(i + 1 < bytes.len() && bytes[i + 1] == b'.') {
has_dot = true;
i += 1;
if i < bytes.len() && bytes[i].is_ascii_digit() {
while i < bytes.len() && digit_or_sep(bytes, i) {
i += 1;
}
}
}
if i < bytes.len() && (bytes[i] == b'e' || bytes[i] == b'E') {
has_exp = true;
i += 1;
if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {
i += 1;
}
let exp_start = i;
if i < bytes.len() && bytes[i].is_ascii_digit() {
while i < bytes.len() && digit_or_sep(bytes, i) {
i += 1;
}
}
if exp_start == i {
return Err(LexErrorKind::BadNumber(s[..i].to_string()));
}
}
junk_check(i)?;
let owned;
let lit: &str = if s[..i].contains('_') {
owned = s[..i].replace('_', "");
&owned
} else {
&s[..i]
};
if has_exp {
Ok((Token::Numeric(lit.to_string()), i))
} else if has_dot {
Ok((Token::Numeric(lit.to_string()), i))
} else {
match lit.parse::<i64>() {
Ok(v) => Ok((Token::Integer(v), i)),
Err(_) => Ok((Token::Numeric(lit.to_string()), i)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
fn lex(s: &str) -> Vec<Token> {
tokenize(s).expect("lex ok")
}
#[test]
fn empty_yields_only_eof() {
assert_eq!(lex(""), vec![Token::Eof]);
}
#[test]
fn whitespace_only_yields_only_eof() {
assert_eq!(lex(" \t\n "), vec![Token::Eof]);
}
#[test]
fn keywords_are_case_insensitive() {
assert_eq!(
lex("SELECT select Select"),
vec![Token::Select, Token::Select, Token::Select, Token::Eof]
);
}
#[test]
fn identifiers_lowercase_ascii() {
assert_eq!(
lex("hello WORLD _x x1"),
vec![
Token::Ident("hello".into()),
Token::Ident("world".into()),
Token::Ident("_x".into()),
Token::Ident("x1".into()),
Token::Eof,
]
);
}
#[test]
fn quoted_identifier_keeps_case_and_handles_embedded_quote() {
assert_eq!(
lex(r#""User Name" "a""b""#),
vec![
Token::QuotedIdent("User Name".into()),
Token::QuotedIdent("a\"b".into()),
Token::Eof,
]
);
}
#[test]
fn integer_and_float_literals() {
assert_eq!(
lex("0 42 1.5 .5 1e10 2.5e-3"),
vec![
Token::Integer(0),
Token::Integer(42),
Token::Numeric("1.5".to_string()),
Token::Numeric(".5".to_string()),
Token::Numeric("1e10".to_string()),
Token::Numeric("2.5e-3".to_string()),
Token::Eof,
]
);
}
#[test]
fn negative_number_is_minus_then_integer() {
assert_eq!(
lex("-42"),
vec![Token::Minus, Token::Integer(42), Token::Eof]
);
}
#[test]
fn string_literal_doubled_quote_escape() {
assert_eq!(
lex("'hello' 'it''s'"),
vec![
Token::String("hello".into()),
Token::String("it's".into()),
Token::Eof,
]
);
}
#[test]
fn all_comparison_and_arithmetic_operators() {
assert_eq!(
lex("= <> != < <= > >= + - * / %"),
vec![
Token::Eq,
Token::NotEq,
Token::NotEq,
Token::Lt,
Token::LtEq,
Token::Gt,
Token::GtEq,
Token::Plus,
Token::Minus,
Token::Star,
Token::Slash,
Token::Percent,
Token::Eof,
]
);
}
#[test]
fn punctuation() {
assert_eq!(
lex("( ) , ; ."),
vec![
Token::LParen,
Token::RParen,
Token::Comma,
Token::Semicolon,
Token::Dot,
Token::Eof,
]
);
}
#[test]
fn line_comment_skipped() {
assert_eq!(
lex("SELECT -- trailing junk\nFROM"),
vec![Token::Select, Token::From, Token::Eof]
);
}
#[test]
fn block_comment_skipped() {
assert_eq!(
lex("SELECT /* skipped */ 1"),
vec![Token::Select, Token::Integer(1), Token::Eof]
);
}
#[test]
fn unterminated_string_errors() {
let err = tokenize("'oops").unwrap_err();
assert!(matches!(err.kind, LexErrorKind::UnterminatedString));
assert_eq!(err.pos, 0);
}
#[test]
fn unterminated_block_comment_errors() {
let err = tokenize("/* never closed").unwrap_err();
assert!(matches!(err.kind, LexErrorKind::UnterminatedBlockComment));
}
#[test]
fn unknown_char_errors() {
let err = tokenize("\x07").unwrap_err();
assert!(matches!(err.kind, LexErrorKind::UnknownChar(_)));
}
#[test]
fn at_alone_lexes_as_punctuation() {
assert_eq!(
lex("'u'@'h'"),
vec![
Token::String("u".into()),
Token::At,
Token::String("h".into()),
Token::Eof,
]
);
}
#[test]
fn dot_in_qualified_column() {
assert_eq!(
lex("t.col"),
vec![
Token::Ident("t".into()),
Token::Dot,
Token::Ident("col".into()),
Token::Eof,
]
);
}
#[test]
fn brackets_are_distinct_tokens() {
assert_eq!(
lex("[ ]"),
vec![Token::LBracket, Token::RBracket, Token::Eof]
);
}
#[test]
fn l2_distance_is_three_char_token() {
assert_eq!(
lex("a <-> b"),
vec![
Token::Ident("a".into()),
Token::L2Distance,
Token::Ident("b".into()),
Token::Eof,
]
);
assert_eq!(
lex("a <- b"),
vec![
Token::Ident("a".into()),
Token::Lt,
Token::Minus,
Token::Ident("b".into()),
Token::Eof,
]
);
}
#[test]
fn order_by_limit_are_keywords() {
assert_eq!(
lex("ORDER BY LIMIT"),
vec![
Token::Order,
Token::Ident("by".into()),
Token::Limit,
Token::Eof,
]
);
}
#[test]
fn inner_product_operator_3char() {
assert_eq!(
lex("a <#> b"),
vec![
Token::Ident("a".into()),
Token::InnerProduct,
Token::Ident("b".into()),
Token::Eof,
]
);
}
#[test]
fn cosine_distance_operator_3char() {
assert_eq!(
lex("a <=> b"),
vec![
Token::Ident("a".into()),
Token::CosineDistance,
Token::Ident("b".into()),
Token::Eof,
]
);
assert_eq!(
lex("a <= b"),
vec![
Token::Ident("a".into()),
Token::LtEq,
Token::Ident("b".into()),
Token::Eof,
]
);
}
#[test]
fn double_colon_cast_token() {
assert_eq!(
lex("x::INT"),
vec![
Token::Ident("x".into()),
Token::DoubleColon,
Token::Ident("int".into()),
Token::Eof,
]
);
}
#[test]
fn lone_single_colon_lexes_as_colon_token() {
let toks = tokenize(":x").expect("colon now lexes");
assert_eq!(toks[0], Token::Colon);
}
#[test]
fn colon_eq_lexes_as_assignment() {
let toks = tokenize("x := 1").expect("colon-eq lexes");
assert!(matches!(toks[1], Token::ColonEq));
}
#[test]
fn pg_escape_string_double_backslash_decodes_to_single() {
let toks = tokenize(r"E'\\xdeadbeef'").expect("E-string lexes");
assert_eq!(toks, vec![Token::String(r"\xdeadbeef".into()), Token::Eof]);
}
#[test]
fn pg_escape_string_supports_basic_escapes() {
let toks = tokenize(r"E'a\nb\tc\'d\\e'").expect("E-string lexes");
assert_eq!(toks, vec![Token::String("a\nb\tc'd\\e".into()), Token::Eof]);
}
#[test]
fn pg_escape_string_hex_byte() {
let toks = tokenize(r"E'\x41B\x42'").expect("E-string lexes");
assert_eq!(toks, vec![Token::String("ABB".into()), Token::Eof]);
}
#[test]
fn pg_escape_string_lowercase_e_prefix() {
let toks = tokenize(r"e'hi\n'").expect("e-string lexes");
assert_eq!(toks, vec![Token::String("hi\n".into()), Token::Eof]);
}
#[test]
fn pg_escape_string_doubled_quote() {
let toks = tokenize(r"E'it''s ok'").expect("E-string lexes");
assert_eq!(toks, vec![Token::String("it's ok".into()), Token::Eof]);
}
}