use crate::error::Span;
use crate::lexer::Lexer;
use crate::token::TokenKind;
fn tokens(source: &str) -> Vec<(TokenKind, &str, Span)> {
Lexer::new(source)
.map(|r| r.unwrap())
.map(|t| (t.kind, t.text, t.span))
.collect()
}
#[test]
fn basic_keywords() {
let t = tokens("QUERY UPSERT DELETE SCROLL");
assert_eq!(t[0].0, TokenKind::Query);
assert_eq!(t[1].0, TokenKind::Upsert);
assert_eq!(t[2].0, TokenKind::Delete);
assert_eq!(t[3].0, TokenKind::Scroll);
}
#[test]
fn unicode_comparison_operators() {
let t = tokens("year ≥ 2024 AND year ≤ 2030 AND id ≠ 5");
assert_eq!(t[1].0, TokenKind::Gte);
assert_eq!(t[5].0, TokenKind::Lte);
assert_eq!(t[9].0, TokenKind::NotEquals);
}
#[test]
fn strings_with_escapes() {
let t = tokens(r"'hello\nworld\t'");
assert_eq!(t[0].0, TokenKind::String);
assert_eq!(t[0].1, r"hello\nworld\t");
let t = tokens(r"'it\'s ok'");
assert_eq!(t[0].0, TokenKind::String);
assert_eq!(t[0].1, r"it\'s ok");
let t = tokens(r"'price is \$100'");
assert_eq!(t[0].0, TokenKind::String);
assert_eq!(t[0].1, r"price is \$100");
}
#[test]
fn raw_and_triple_quoted_and_backtick_strings() {
let t = tokens(r"r'QUERY $QUERY_TEXT FROM docs USING dense LIMIT $LIMIT;'");
assert_eq!(t[0].0, TokenKind::String);
assert_eq!(
t[0].1,
r"QUERY $QUERY_TEXT FROM docs USING dense LIMIT $LIMIT;"
);
let t = tokens("'''QUERY '$QUERY_TEXT'\nFROM berlin_airbnb\nLIMIT $LIMIT;'''");
assert_eq!(t[0].0, TokenKind::String);
assert_eq!(
t[0].1,
"QUERY '$QUERY_TEXT'\nFROM berlin_airbnb\nLIMIT $LIMIT;"
);
let t = tokens("`QUERY '$QUERY_TEXT'`");
assert_eq!(t[0].0, TokenKind::String);
assert_eq!(t[0].1, "QUERY '$QUERY_TEXT'");
}
#[test]
fn string_span_covers_quotes() {
let t = tokens("'hello' FROM docs");
assert_eq!(t[0].2, Span::new(0, 7));
}
#[test]
fn scientific_notation_floats() {
let t = tokens("1e-5 1.2e+3 1.0e-05 -5e2");
assert_eq!(t[0].0, TokenKind::Float);
assert_eq!(t[0].1, "1e-5");
assert_eq!(t[1].0, TokenKind::Float);
assert_eq!(t[1].1, "1.2e+3");
assert_eq!(t[2].0, TokenKind::Float);
assert_eq!(t[2].1, "1.0e-05");
assert_eq!(t[3].0, TokenKind::Float);
assert_eq!(t[3].1, "-5e2");
}
#[test]
fn sql_style_escaped_strings() {
let t = tokens("'St. Peter''s Church'");
assert_eq!(t[0].0, TokenKind::String);
assert_eq!(t[0].1, "St. Peter''s Church");
}
#[test]
fn numbers_integer_and_float() {
let t = tokens("42 3.14 -5 -0.5");
assert_eq!(t[0].0, TokenKind::Integer);
assert_eq!(t[0].1, "42");
assert_eq!(t[1].0, TokenKind::Float);
assert_eq!(t[1].1, "3.14");
assert_eq!(t[2].0, TokenKind::Integer);
assert_eq!(t[2].1, "-5");
assert_eq!(t[3].0, TokenKind::Float);
assert_eq!(t[3].1, "-0.5");
}
#[test]
fn minus_as_operator() {
let t = tokens("a - b");
assert_eq!(t[1].0, TokenKind::Minus);
}
#[test]
fn identifiers_and_keywords() {
let t = tokens("QUERY my_field FROM docs WHERE x = 1");
assert_eq!(t[0].0, TokenKind::Query);
assert_eq!(t[1].0, TokenKind::Identifier);
assert_eq!(t[1].1, "my_field");
assert_eq!(t[2].0, TokenKind::From);
assert_eq!(t[3].0, TokenKind::Identifier);
assert_eq!(t[3].1, "docs");
assert_eq!(t[4].0, TokenKind::Where);
assert_eq!(t[6].0, TokenKind::Equals);
}
#[test]
fn dollar_identifiers() {
let t = tokens("$score $my_var");
assert_eq!(t[0].0, TokenKind::Identifier);
assert_eq!(t[0].1, "$score");
assert_eq!(t[1].0, TokenKind::Identifier);
assert_eq!(t[1].1, "$my_var");
}
#[test]
fn line_comments_skipped() {
let source = "QUERY -- this is a comment\nFROM docs";
let t = tokens(source);
assert_eq!(t[0].0, TokenKind::Query);
assert_eq!(t[1].0, TokenKind::From);
assert_eq!(t[2].0, TokenKind::Identifier);
assert_eq!(t[2].1, "docs");
}
#[test]
fn comparison_operators() {
let t = tokens("= != > >= < <=");
assert_eq!(t[0].0, TokenKind::Equals);
assert_eq!(t[1].0, TokenKind::NotEquals);
assert_eq!(t[2].0, TokenKind::Gt);
assert_eq!(t[3].0, TokenKind::Gte);
assert_eq!(t[4].0, TokenKind::Lt);
assert_eq!(t[5].0, TokenKind::Lte);
}
#[test]
fn brackets_and_delimiters() {
let t = tokens("{ } [ ] ( ) , : ;");
assert_eq!(t[0].0, TokenKind::Lbrace);
assert_eq!(t[1].0, TokenKind::Rbrace);
assert_eq!(t[2].0, TokenKind::Lbracket);
assert_eq!(t[3].0, TokenKind::Rbracket);
assert_eq!(t[4].0, TokenKind::Lparen);
assert_eq!(t[5].0, TokenKind::Rparen);
assert_eq!(t[6].0, TokenKind::Comma);
assert_eq!(t[7].0, TokenKind::Colon);
assert_eq!(t[8].0, TokenKind::Semicolon);
}
#[test]
fn span_positions_are_bytes() {
let source = "QUERY docs";
let t = tokens(source);
assert_eq!(t[0].2, Span::new(0, 5));
assert_eq!(t[1].2, Span::new(6, 10));
}
#[test]
fn identifier_with_underscore_and_digits() {
let t = tokens("field_name field2 _private");
assert!(t.iter().all(|(k, _, _)| matches!(k, TokenKind::Identifier)));
}
#[test]
fn invalid_character_errors() {
let mut lexer = Lexer::new("@invalid");
assert!(lexer.next().unwrap().is_err());
let mut lexer = Lexer::new("#");
assert!(lexer.next().unwrap().is_err());
}