use rudb_common::{Error, Result, Span};
use crate::generated::keywords::{KEYWORDS, LONGEST};
use crate::token::{Flags, Kind, NOT_A_KEYWORD, Token};
pub fn tokenize(query: &str) -> Result<Vec<Token>> {
Tokenizer::new(query).run()
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum State {
Standard,
LineComment,
BlockComment,
QuotedIdentifier,
StringLiteral,
Word,
Numeric,
Operator,
DollarQuoted,
}
struct Tokenizer<'a> {
query: &'a str,
bytes: &'a [u8],
tokens: Vec<Token>,
last: usize,
block_comment_at: Option<usize>,
escape_string: bool,
dollar_tag: Span,
depth: u32,
}
impl<'a> Tokenizer<'a> {
fn new(query: &'a str) -> Self {
Tokenizer {
query,
bytes: query.as_bytes(),
tokens: Vec::with_capacity(query.len() / 4 + 4),
last: 0,
block_comment_at: None,
escape_string: false,
dollar_tag: Span::new(0, 0),
depth: 0,
}
}
fn run(mut self) -> Result<Vec<Token>> {
let mut state = State::Standard;
let mut i = 0;
while i < self.bytes.len() {
let c = self.bytes[i];
match state {
State::Standard => {
if let Some(next) = self.standard(&mut i, c)? {
state = next;
}
}
State::Numeric => self.numeric(&mut state, &mut i, c),
State::Operator => self.operator(&mut state, &mut i, c),
State::Word => self.word(&mut state, &mut i, c),
State::StringLiteral => self.string_literal(&mut state, &mut i, c),
State::QuotedIdentifier => self.quoted_identifier(&mut state, &mut i, c)?,
State::LineComment => {
if c == b'\n' || c == b'\r' {
self.comment(self.last, i + 1);
self.last = i + 1;
state = State::Standard;
}
}
State::BlockComment => self.block_comment(&mut state, &mut i, c),
State::DollarQuoted => self.dollar_quoted(&mut state, &mut i),
}
i += 1;
}
self.finish(state)
}
fn finish(mut self, state: State) -> Result<Vec<Token>> {
let end = self.bytes.len();
match state {
State::LineComment => {
self.comment(self.last, end);
}
State::BlockComment => {
return Err(self.error(
format!(
"unterminated /* comment at or near \"{}\"",
&self.query[self.last..end]
),
self.last,
));
}
State::Operator => self.push_operator(self.last, end),
State::DollarQuoted => {
self.push_flagged(self.last, end, Kind::String, Flags::UNTERMINATED);
}
State::StringLiteral => {
return Err(self.error("unterminated string literal", self.last));
}
State::QuotedIdentifier => {
return Err(self.error("unterminated quoted identifier", self.last));
}
State::Numeric => self.push(self.last, end, Kind::Number),
State::Word => self.push_word(self.last, end),
State::Standard => self.push(self.last, end, Kind::Identifier),
}
self.tokens.push(Token {
kind: Kind::EndOfInput,
flags: Flags::default(),
keyword: NOT_A_KEYWORD,
start: end as u32,
end: end as u32,
});
Ok(self.tokens)
}
fn standard(&mut self, i: &mut usize, c: u8) -> Result<Option<State>> {
match c {
b'\'' => {
self.last = *i;
self.escape_string = false;
return Ok(Some(State::StringLiteral));
}
b'"' => {
self.last = *i;
return Ok(Some(State::QuotedIdentifier));
}
b';' => {
self.tokens.push(Token {
kind: Kind::Terminator,
flags: self.gap_flags(*i),
keyword: NOT_A_KEYWORD,
start: *i as u32,
end: *i as u32 + 1,
});
self.last = *i + 1;
return Ok(None);
}
b'$' => return Ok(self.dollar(i)),
b'-' if self.bytes.get(*i + 1) == Some(&b'-') => {
*i += 1;
return Ok(Some(State::LineComment));
}
b'/' if self.bytes.get(*i + 1) == Some(&b'*') => {
*i += 1;
self.depth = 1;
return Ok(Some(State::BlockComment));
}
_ => {}
}
if is_space(c) {
self.last = *i + 1;
return Ok(None);
}
if let Some(len) = special_operator(self.bytes, *i) {
if self.bytes.get(*i + len).is_some_and(|&next| is_operator_char_in_run(next)) {
self.last = *i;
return Ok(Some(State::Operator));
}
self.push(*i, *i + len, Kind::Operator);
*i += len - 1;
self.last = *i + 1;
return Ok(None);
}
if is_single_byte_operator(c) {
self.push(*i, *i + 1, Kind::Operator);
self.last = *i + 1;
return Ok(None);
}
if is_initial_number(c) {
self.last = *i;
return Ok(Some(State::Numeric));
}
if is_string_prefix(c) && self.bytes.get(*i + 1) == Some(&b'\'') {
self.last = *i;
self.escape_string = c == b'E' || c == b'e';
*i += 1;
return Ok(Some(State::StringLiteral));
}
if is_operator_char(c) {
self.last = *i;
return Ok(Some(State::Operator));
}
self.last = *i;
Ok(Some(State::Word))
}
fn dollar(&mut self, i: &mut usize) -> Option<State> {
let Some(&next) = self.bytes.get(*i + 1) else {
return None;
};
if next.is_ascii_digit() {
self.push(*i, *i + 1, Kind::Operator);
return None;
}
let mut close = None;
for at in *i + 1..self.bytes.len() {
if self.bytes[at] == b'$' {
close = Some(at);
break;
}
if !is_dollar_tag_char(self.bytes[at]) {
break;
}
}
let Some(close) = close else {
self.push(*i, *i + 1, Kind::Operator);
return None;
};
self.last = *i;
self.dollar_tag = Span::new(*i as u32 + 1, close as u32);
*i = close;
Some(State::DollarQuoted)
}
fn numeric(&mut self, state: &mut State, i: &mut usize, c: u8) {
if is_initial_number(c) {
return;
}
if c == b'_' && self.bytes.get(*i + 1).is_some_and(|&n| is_initial_number(n)) {
return;
}
if is_scientific(c) && !is_scientific(self.bytes[*i - 1]) {
if self.bytes[self.last].is_ascii_digit() || self.bytes[*i - 1].is_ascii_digit() {
return;
}
}
if (c == b'+' || c == b'-') && is_scientific(self.bytes[*i - 1]) {
return;
}
while !is_initial_number(self.bytes[*i - 1]) {
*i -= 1;
}
self.push(self.last, *i, Kind::Number);
*state = State::Standard;
self.last = *i;
*i -= 1;
}
fn operator(&mut self, state: &mut State, i: &mut usize, c: u8) {
if c == b'/' && self.bytes.get(*i + 1) == Some(&b'*') {
self.push_operator(self.last, *i);
*state = State::Standard;
self.last = *i;
*i -= 1;
return;
}
if !is_operator_char_in_run(c) {
self.push_operator(self.last, *i);
*state = State::Standard;
self.last = *i;
*i -= 1;
}
}
fn word(&mut self, state: &mut State, i: &mut usize, c: u8) {
if c == b'$' || is_word_char(c) {
return;
}
self.push_word(self.last, *i);
*state = State::Standard;
self.last = *i;
*i -= 1;
}
fn string_literal(&mut self, state: &mut State, i: &mut usize, c: u8) {
if self.escape_string && c == b'\\' && *i + 1 < self.bytes.len() {
*i += 1;
return;
}
if c != b'\'' {
return;
}
if self.bytes.get(*i + 1) == Some(&b'\'') {
*i += 1;
return;
}
self.push(self.last, *i + 1, Kind::String);
self.last = *i + 1;
self.escape_string = false;
*state = State::Standard;
}
fn quoted_identifier(&mut self, state: &mut State, i: &mut usize, c: u8) -> Result<()> {
if c != b'"' {
return Ok(());
}
if self.bytes.get(*i + 1) == Some(&b'"') {
*i += 1;
return Ok(());
}
if *i + 1 == self.last + 2 {
return Err(self.error("zero-length delimited identifier", self.last));
}
self.push(self.last, *i + 1, Kind::QuotedIdentifier);
self.last = *i + 1;
*state = State::Standard;
Ok(())
}
fn block_comment(&mut self, state: &mut State, i: &mut usize, c: u8) {
if c == b'/' && self.bytes.get(*i + 1) == Some(&b'*') {
*i += 1;
self.depth += 1;
} else if c == b'*' && self.bytes.get(*i + 1) == Some(&b'/') {
*i += 1;
self.depth -= 1;
if self.depth == 0 {
self.comment(self.last, *i + 1);
self.last = *i + 1;
*state = State::Standard;
}
}
}
fn dollar_quoted(&mut self, state: &mut State, i: &mut usize) {
if self.bytes[*i] != b'$' || *i + 1 >= self.bytes.len() {
return;
}
let start = *i + 1;
let mut end = start;
while end < self.bytes.len() && self.bytes[end] != b'$' {
end += 1;
}
if end >= self.bytes.len() {
return;
}
let tag = &self.bytes[self.dollar_tag.start as usize..self.dollar_tag.end as usize];
if end - start != tag.len() || &self.bytes[start..end] != tag {
return;
}
self.push(self.last, end + 1, Kind::String);
*state = State::Standard;
*i = end;
self.last = *i + 1;
}
fn push_word(&mut self, start: usize, end: usize) {
if start >= end {
return;
}
let keyword = lookup(&self.query[start..end]);
let kind = if classes(keyword) == 0 { Kind::Identifier } else { Kind::Keyword };
let flags = self.gap_flags(start);
self.tokens.push(Token { kind, flags, keyword, start: start as u32, end: end as u32 });
}
fn push_operator(&mut self, start: usize, end: usize) {
let special = self.bytes[start..end].iter().any(|&b| {
matches!(b, b'~' | b'!' | b'@' | b'#' | b'%' | b'^' | b'&' | b'|' | b'`' | b'?')
});
let mut cut = end;
if !special {
while cut > start && self.bytes[cut - 1] == b'+' {
cut -= 1;
}
}
self.push(start, cut, Kind::Operator);
for at in cut..end {
self.push(at, at + 1, Kind::Operator);
}
}
fn comment(&mut self, start: usize, end: usize) {
if end >= start + 2 && &self.bytes[start..start + 2] == b"/*" {
self.block_comment_at = Some(start);
}
}
fn push(&mut self, start: usize, end: usize, kind: Kind) {
if start >= end {
return;
}
let flags = self.gap_flags(start);
self.tokens.push(Token {
kind,
flags,
keyword: NOT_A_KEYWORD,
start: start as u32,
end: end as u32,
});
}
fn push_flagged(&mut self, start: usize, end: usize, kind: Kind, extra: Flags) {
self.push(start, end, kind);
if let Some(token) = self.tokens.last_mut() {
token.flags = token.flags.with(extra);
}
}
fn gap_flags(&self, start: usize) -> Flags {
let Some(previous) = self.tokens.last() else { return Flags::default() };
let from = previous.end as usize;
let mut flags = Flags::default();
if self.block_comment_at.is_some_and(|at| at >= from && at < start) {
flags = flags.with(Flags::BLOCK_COMMENT);
}
if self.bytes[from..start.min(self.bytes.len())].iter().any(|&b| b == b'\n' || b == b'\r') {
flags = flags.with(Flags::NEWLINE);
}
flags
}
fn error(&self, message: impl Into<String>, at: usize) -> Error {
Error::parser(message).with_span(Span::new(at as u32, self.bytes.len() as u32))
}
}
pub fn lookup(word: &str) -> u16 {
if word.len() > LONGEST {
return NOT_A_KEYWORD;
}
let mut folded = [0u8; LONGEST];
for (slot, byte) in folded.iter_mut().zip(word.bytes()) {
*slot = byte.to_ascii_lowercase();
}
let folded = &folded[..word.len()];
match KEYWORDS.binary_search_by(|(candidate, _)| candidate.as_bytes().cmp(folded)) {
Ok(at) => at as u16,
Err(_) => NOT_A_KEYWORD,
}
}
pub fn classes(keyword: u16) -> u8 {
if keyword == NOT_A_KEYWORD { 0 } else { KEYWORDS[keyword as usize].1 }
}
const fn is_space(c: u8) -> bool {
matches!(c, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')
}
const fn is_single_byte_operator(c: u8) -> bool {
matches!(c, b'(' | b')' | b'{' | b'}' | b'[' | b']' | b',' | b'?' | b'$' | b'-' | b'#')
}
const fn is_operator_char(c: u8) -> bool {
if c == b'_' {
return false;
}
matches!(c, b'!'..=b'/' | b':'..=b'@' | b'['..=b'`' | b'{'..=b'~')
}
const fn is_operator_char_in_run(c: u8) -> bool {
if is_single_byte_operator(c) || is_control_flow(c) {
return false;
}
is_operator_char(c)
}
const fn is_control_flow(c: u8) -> bool {
matches!(c, b'\'' | b'-' | b';' | b'"' | b'.')
}
const fn is_word_char(c: u8) -> bool {
if is_single_byte_operator(c) || is_operator_char(c) || is_space(c) || is_control_flow(c) {
return false;
}
true
}
const fn is_initial_number(c: u8) -> bool {
c.is_ascii_digit() || c == b'.'
}
const fn is_scientific(c: u8) -> bool {
c == b'e' || c == b'E'
}
const fn is_string_prefix(c: u8) -> bool {
matches!(c, b'N' | b'n' | b'X' | b'x' | b'E' | b'e' | b'B' | b'b')
}
const fn is_dollar_tag_char(c: u8) -> bool {
c.is_ascii_alphanumeric() || c == b'_' || c >= 0x80
}
fn special_operator(bytes: &[u8], at: usize) -> Option<usize> {
if bytes[at..].starts_with(b"->>") {
return Some(3);
}
for candidate in [b"::".as_slice(), b":=", b"->", b"**", b"//"] {
if bytes[at..].starts_with(candidate) {
return Some(2);
}
}
None
}
#[cfg(test)]
mod tests {
use super::{classes, lookup, tokenize};
use crate::generated::keywords::{RESERVED, UNRESERVED};
use crate::token::{Flags, Kind, NOT_A_KEYWORD, Token};
fn scan(query: &str) -> Vec<(Kind, &str)> {
let tokens = tokenize(query).expect("tokenizes");
assert_eq!(tokens.last().map(|t| t.kind), Some(Kind::EndOfInput));
assert_eq!(tokens.iter().filter(|t| t.kind == Kind::EndOfInput).count(), 1);
tokens[..tokens.len() - 1].iter().map(|t| (t.kind, t.text(query))).collect()
}
fn texts(query: &str) -> Vec<&str> {
scan(query).into_iter().map(|(_, text)| text).collect()
}
fn all(query: &str) -> Vec<Token> {
tokenize(query).expect("tokenizes")
}
fn message(query: &str) -> String {
tokenize(query).expect_err("fails").message().to_string()
}
#[test]
fn the_empty_query_is_one_sentinel() {
let tokens = tokenize("").expect("tokenizes");
assert_eq!(tokens.len(), 1);
assert_eq!(tokens[0].kind, Kind::EndOfInput);
assert_eq!(tokens[0].span(), rudb_common::Span::new(0, 0));
assert!(scan(" \t\n ").is_empty());
}
#[test]
fn a_word_in_a_class_is_a_keyword_and_one_in_none_is_not() {
assert_eq!(scan("SELECT"), [(Kind::Keyword, "SELECT")]);
assert_eq!(scan("banana"), [(Kind::Identifier, "banana")]);
}
#[test]
fn case_is_matched_but_not_folded() {
for spelling in ["select", "SELECT", "SeLeCt"] {
let tokens = all(spelling);
assert_eq!(tokens[0].kind, Kind::Keyword);
assert_eq!(tokens[0].text(spelling), spelling);
assert_eq!(tokens[0].keyword, lookup("select"));
}
assert_eq!(scan(r#""Foo""#), [(Kind::QuotedIdentifier, r#""Foo""#)]);
}
#[test]
fn a_soft_word_keeps_its_index_and_stays_an_identifier() {
let tokens = all("ascending");
assert_eq!(tokens[0].kind, Kind::Identifier);
assert_ne!(tokens[0].keyword, NOT_A_KEYWORD);
assert_eq!(classes(tokens[0].keyword), 0);
let tokens = all("banana");
assert_eq!(tokens[0].keyword, NOT_A_KEYWORD);
assert_eq!(classes(tokens[0].keyword), 0);
}
#[test]
fn the_classes_come_back_off_the_index() {
assert_eq!(classes(lookup("select")) & RESERVED, RESERVED);
assert_eq!(classes(lookup("abort")) & UNRESERVED, UNRESERVED);
assert_eq!(lookup("supercalifragilistic"), NOT_A_KEYWORD);
assert_eq!(lookup(""), NOT_A_KEYWORD);
}
#[test]
fn a_quoted_identifier_is_never_a_keyword() {
let tokens = all(r#""select""#);
assert_eq!(tokens[0].kind, Kind::QuotedIdentifier);
assert_eq!(tokens[0].keyword, NOT_A_KEYWORD);
assert_eq!(scan(r#""a""b""#), [(Kind::QuotedIdentifier, r#""a""b""#)]);
}
#[test]
fn a_dollar_is_an_identifier_character_after_the_first_byte() {
assert_eq!(scan("a$b"), [(Kind::Identifier, "a$b")]);
}
#[test]
fn any_byte_above_ascii_is_an_identifier_character() {
assert_eq!(scan("SELECT café"), [(Kind::Keyword, "SELECT"), (Kind::Identifier, "café")]);
}
#[test]
fn a_number_swallows_more_than_a_number() {
assert_eq!(scan("1.2.3"), [(Kind::Number, "1.2.3")]);
assert_eq!(scan("1_000"), [(Kind::Number, "1_000")]);
assert_eq!(scan("1e5"), [(Kind::Number, "1e5")]);
assert_eq!(scan(".1e5"), [(Kind::Number, ".1e5")]);
assert_eq!(scan("1e-5"), [(Kind::Number, "1e-5")]);
assert_eq!(scan("1.e5"), [(Kind::Number, "1.e5")]);
}
#[test]
fn a_trailing_e_stays_on_the_number() {
assert_eq!(scan("SELECT 1e"), [(Kind::Keyword, "SELECT"), (Kind::Number, "1e")]);
assert_eq!(scan("SELECT 1e+"), [(Kind::Keyword, "SELECT"), (Kind::Number, "1e+")]);
}
#[test]
fn what_the_number_cannot_use_it_gives_back() {
assert_eq!(
scan("SELECT 1e+ 1"),
[
(Kind::Keyword, "SELECT"),
(Kind::Number, "1"),
(Kind::Identifier, "e"),
(Kind::Operator, "+"),
(Kind::Number, "1"),
]
);
assert_eq!(scan("1_"), [(Kind::Number, "1"), (Kind::Identifier, "_")]);
assert_eq!(scan("1__0"), [(Kind::Number, "1"), (Kind::Identifier, "__0")]);
assert_eq!(scan("0x1F"), [(Kind::Number, "0"), (Kind::Identifier, "x1F")]);
assert_eq!(scan(".e100"), [(Kind::Number, "."), (Kind::Identifier, "e100")]);
}
#[test]
fn a_minus_never_joins_an_operator_run() {
assert_eq!(texts("1-1"), ["1", "-", "1"]);
assert_eq!(texts("SELECT 1 =- 1"), ["SELECT", "1", "=", "-", "1"]);
assert_eq!(texts("(a,b)"), ["(", "a", ",", "b", ")"]);
}
#[test]
fn the_postgres_plus_rule_decides_where_a_run_ends() {
assert_eq!(texts("SELECT 1 =+ 1"), ["SELECT", "1", "=", "+", "1"]);
assert_eq!(texts("SELECT 1 !=+ 1"), ["SELECT", "1", "!=+", "1"]);
assert_eq!(texts("SELECT 1 =++ 1"), ["SELECT", "1", "=", "+", "+", "1"]);
assert_eq!(texts("SELECT 1 ++ 1"), ["SELECT", "1", "+", "+", "1"]);
}
#[test]
fn the_special_operators_are_checked_before_the_run() {
assert_eq!(texts("a->>'b'"), ["a", "->>", "'b'"]);
assert_eq!(texts("a->'b'"), ["a", "->", "'b'"]);
assert_eq!(texts("a::b"), ["a", "::", "b"]);
assert_eq!(texts("a//b"), ["a", "//", "b"]);
assert_eq!(texts("2**3"), ["2", "**", "3"]);
assert_eq!(texts("a::=b"), ["a", "::=", "b"]);
}
#[test]
fn a_block_comment_can_end_an_operator_run() {
assert_eq!(texts("1+/*c*/2"), ["1", "+", "2"]);
}
#[test]
fn a_comment_is_not_a_token_but_a_block_one_leaves_a_mark() {
assert_eq!(texts("SELECT --x\n1"), ["SELECT", "1"]);
assert_eq!(texts("SELECT /*x*/ 1"), ["SELECT", "1"]);
assert_eq!(texts("SELECT --x"), ["SELECT"]);
let tokens = all("SELECT /*x*/ 1");
assert!(tokens[1].flags.has(Flags::BLOCK_COMMENT));
assert!(!tokens[1].flags.has(Flags::NEWLINE));
let tokens = all("SELECT --x\n1");
assert!(!tokens[1].flags.has(Flags::BLOCK_COMMENT));
assert!(tokens[1].flags.has(Flags::NEWLINE));
}
#[test]
fn the_first_token_is_preceded_by_nothing() {
let tokens = all("\n/*x*/ SELECT");
assert_eq!(tokens[0].flags, Flags::default());
}
#[test]
fn block_comments_nest() {
assert_eq!(texts("SELECT /* a /* b */ c */ 1"), ["SELECT", "1"]);
assert_eq!(
message("SELECT /* a /* b */ 1"),
"unterminated /* comment at or near \"/* a /* b */ 1\""
);
}
#[test]
fn a_string_keeps_its_quotes_and_its_escapes() {
assert_eq!(scan("'it''s'"), [(Kind::String, "'it''s'")]);
assert_eq!(scan("''"), [(Kind::String, "''")]);
assert_eq!(texts("'a' 'b'"), ["'a'", "'b'"]);
}
#[test]
fn only_the_e_prefix_changes_how_a_string_is_read() {
assert_eq!(scan(r"E'\''"), [(Kind::String, r"E'\''")]);
assert_eq!(message(r"'\''"), "unterminated string literal");
for prefix in ["X", "x", "B", "b", "N", "n", "E", "e"] {
let query = format!("{prefix}'a'");
assert_eq!(tokenize(&query).expect("tokenizes")[0].kind, Kind::String);
}
assert_eq!(texts("x 'a'"), ["x", "'a'"]);
}
#[test]
fn a_dollar_quoted_string_is_one_token_and_its_tag_has_to_match() {
assert_eq!(scan("$$abc$$"), [(Kind::String, "$$abc$$")]);
assert_eq!(scan("$tag$abc$tag$"), [(Kind::String, "$tag$abc$tag$")]);
assert_eq!(scan("$tag$a$other$b$tag$"), [(Kind::String, "$tag$a$other$b$tag$")]);
assert_eq!(scan("$$it's fine$$"), [(Kind::String, "$$it's fine$$")]);
}
#[test]
fn an_unterminated_dollar_quote_is_a_token_and_not_an_error() {
let tokens = all("$$abc");
assert_eq!(tokens[0].kind, Kind::String);
assert!(tokens[0].flags.has(Flags::UNTERMINATED));
assert_eq!(tokens[0].text("$$abc"), "$$abc");
}
#[test]
fn a_parameter_is_two_tokens() {
assert_eq!(scan("$1"), [(Kind::Operator, "$"), (Kind::Number, "1")]);
assert_eq!(scan("$banana"), [(Kind::Operator, "$"), (Kind::Identifier, "banana")]);
assert_eq!(scan("?"), [(Kind::Operator, "?")]);
assert_eq!(scan("$"), [(Kind::Identifier, "$")]);
}
#[test]
fn a_semicolon_is_its_own_kind() {
assert_eq!(
scan("SELECT 1; SELECT 2"),
[
(Kind::Keyword, "SELECT"),
(Kind::Number, "1"),
(Kind::Terminator, ";"),
(Kind::Keyword, "SELECT"),
(Kind::Number, "2"),
]
);
assert_eq!(scan(";"), [(Kind::Terminator, ";")]);
}
#[test]
fn the_four_errors_are_the_four_upstream_throws() {
assert_eq!(message("SELECT /* x"), "unterminated /* comment at or near \"/* x\"");
assert_eq!(message("SELECT 'x"), "unterminated string literal");
assert_eq!(message("SELECT \"x"), "unterminated quoted identifier");
assert_eq!(message("SELECT \"\""), "zero-length delimited identifier");
assert_eq!(tokenize("SELECT 'x").expect_err("fails").span().map(|s| s.start), Some(7));
}
#[test]
fn every_span_lands_where_the_text_is() {
let query = "SELECT a, /*c*/ 'b' || $$d$$ FROM t;";
for token in tokenize(query).expect("tokenizes") {
assert!(token.end as usize <= query.len());
assert!(token.start <= token.end);
if token.kind != Kind::EndOfInput {
assert!(!token.text(query).is_empty());
}
}
}
#[test]
fn a_real_query_comes_out_the_way_it_reads() {
assert_eq!(
scan("SELECT count(*) FROM t WHERE x > 5 AND y::VARCHAR = 'a';"),
[
(Kind::Keyword, "SELECT"),
(Kind::Identifier, "count"),
(Kind::Operator, "("),
(Kind::Operator, "*"),
(Kind::Operator, ")"),
(Kind::Keyword, "FROM"),
(Kind::Identifier, "t"),
(Kind::Keyword, "WHERE"),
(Kind::Identifier, "x"),
(Kind::Operator, ">"),
(Kind::Number, "5"),
(Kind::Keyword, "AND"),
(Kind::Identifier, "y"),
(Kind::Operator, "::"),
(Kind::Keyword, "VARCHAR"),
(Kind::Operator, "="),
(Kind::String, "'a'"),
(Kind::Terminator, ";"),
]
);
}
}