1use rudb_common::{Error, Result, Span};
26
27use crate::generated::keywords::{KEYWORDS, LONGEST};
28use crate::token::{Flags, Kind, NOT_A_KEYWORD, Token};
29
30pub fn tokenize(query: &str) -> Result<Vec<Token>> {
37 Ok(Tokenizer::new(query).run()?.0)
38}
39
40pub fn hints(query: &str) -> Result<Vec<&str>> {
56 if !query.contains("/*+") {
57 return Ok(Vec::new());
58 }
59 let (_, spans) = Tokenizer::new(query).run()?;
60 Ok(spans.iter().map(|span| &query[span.start as usize..span.end as usize]).collect())
61}
62
63#[derive(Clone, Copy, PartialEq, Eq)]
68enum State {
69 Standard,
70 LineComment,
71 BlockComment,
72 QuotedIdentifier,
73 StringLiteral,
74 Word,
75 Numeric,
76 Operator,
77 DollarQuoted,
78}
79
80struct Tokenizer<'a> {
81 query: &'a str,
82 bytes: &'a [u8],
83 tokens: Vec<Token>,
84 last: usize,
86 block_comment_at: Option<usize>,
89 hints: Vec<Span>,
91 escape_string: bool,
93 dollar_tag: Span,
95 depth: u32,
96}
97
98impl<'a> Tokenizer<'a> {
99 fn new(query: &'a str) -> Self {
100 Tokenizer {
101 query,
102 bytes: query.as_bytes(),
103 tokens: Vec::with_capacity(query.len() / 4 + 4),
106 last: 0,
107 block_comment_at: None,
108 hints: Vec::new(),
109 escape_string: false,
110 dollar_tag: Span::new(0, 0),
111 depth: 0,
112 }
113 }
114
115 fn run(mut self) -> Result<(Vec<Token>, Vec<Span>)> {
116 let mut state = State::Standard;
117 let mut i = 0;
118 while i < self.bytes.len() {
119 let c = self.bytes[i];
120 match state {
121 State::Standard => {
122 if let Some(next) = self.standard(&mut i, c)? {
123 state = next;
124 }
125 }
126 State::Numeric => self.numeric(&mut state, &mut i, c),
127 State::Operator => self.operator(&mut state, &mut i, c),
128 State::Word => self.word(&mut state, &mut i, c),
129 State::StringLiteral => self.string_literal(&mut state, &mut i, c),
130 State::QuotedIdentifier => self.quoted_identifier(&mut state, &mut i, c)?,
131 State::LineComment => {
132 if c == b'\n' || c == b'\r' {
133 self.comment(self.last, i + 1);
134 self.last = i + 1;
135 state = State::Standard;
136 }
137 }
138 State::BlockComment => self.block_comment(&mut state, &mut i, c),
139 State::DollarQuoted => self.dollar_quoted(&mut state, &mut i),
140 }
141 i += 1;
142 }
143 self.finish(state)
144 }
145
146 fn finish(mut self, state: State) -> Result<(Vec<Token>, Vec<Span>)> {
148 let end = self.bytes.len();
149 match state {
150 State::LineComment => {
151 self.comment(self.last, end);
152 }
153 State::BlockComment => {
154 return Err(self.error(
155 format!(
156 "unterminated /* comment at or near \"{}\"",
157 &self.query[self.last..end]
158 ),
159 self.last,
160 ));
161 }
162 State::Operator => self.push_operator(self.last, end),
163 State::DollarQuoted => {
164 self.push_flagged(self.last, end, Kind::String, Flags::UNTERMINATED);
167 }
168 State::StringLiteral => {
169 return Err(self.error("unterminated string literal", self.last));
170 }
171 State::QuotedIdentifier => {
172 return Err(self.error("unterminated quoted identifier", self.last));
173 }
174 State::Numeric => self.push(self.last, end, Kind::Number),
175 State::Word => self.push_word(self.last, end),
176 State::Standard => self.push(self.last, end, Kind::Identifier),
180 }
181 self.tokens.push(Token {
182 kind: Kind::EndOfInput,
183 flags: Flags::default(),
184 keyword: NOT_A_KEYWORD,
185 start: end as u32,
186 end: end as u32,
187 });
188 Ok((self.tokens, self.hints))
189 }
190
191 fn standard(&mut self, i: &mut usize, c: u8) -> Result<Option<State>> {
193 match c {
194 b'\'' => {
195 self.last = *i;
196 self.escape_string = false;
197 return Ok(Some(State::StringLiteral));
198 }
199 b'"' => {
200 self.last = *i;
201 return Ok(Some(State::QuotedIdentifier));
202 }
203 b';' => {
204 self.tokens.push(Token {
208 kind: Kind::Terminator,
209 flags: self.gap_flags(*i),
210 keyword: NOT_A_KEYWORD,
211 start: *i as u32,
212 end: *i as u32 + 1,
213 });
214 self.last = *i + 1;
215 return Ok(None);
216 }
217 b'$' => return Ok(self.dollar(i)),
218 b'-' if self.bytes.get(*i + 1) == Some(&b'-') => {
219 *i += 1;
220 return Ok(Some(State::LineComment));
221 }
222 b'/' if self.bytes.get(*i + 1) == Some(&b'*') => {
223 *i += 1;
224 self.depth = 1;
225 return Ok(Some(State::BlockComment));
226 }
227 _ => {}
228 }
229
230 if is_space(c) {
231 self.last = *i + 1;
232 return Ok(None);
233 }
234
235 if let Some(len) = special_operator(self.bytes, *i) {
236 if self.bytes.get(*i + len).is_some_and(|&next| is_operator_char_in_run(next)) {
239 self.last = *i;
240 return Ok(Some(State::Operator));
241 }
242 self.push(*i, *i + len, Kind::Operator);
243 *i += len - 1;
244 self.last = *i + 1;
245 return Ok(None);
246 }
247
248 if is_single_byte_operator(c) {
249 self.push(*i, *i + 1, Kind::Operator);
250 self.last = *i + 1;
251 return Ok(None);
252 }
253
254 if is_initial_number(c) {
255 self.last = *i;
256 return Ok(Some(State::Numeric));
257 }
258
259 if is_string_prefix(c) && self.bytes.get(*i + 1) == Some(&b'\'') {
263 self.last = *i;
264 self.escape_string = c == b'E' || c == b'e';
265 *i += 1;
266 return Ok(Some(State::StringLiteral));
267 }
268
269 if is_operator_char(c) {
270 self.last = *i;
271 return Ok(Some(State::Operator));
272 }
273
274 self.last = *i;
275 Ok(Some(State::Word))
276 }
277
278 fn dollar(&mut self, i: &mut usize) -> Option<State> {
280 let Some(&next) = self.bytes.get(*i + 1) else {
281 return None;
284 };
285 if next.is_ascii_digit() {
286 self.push(*i, *i + 1, Kind::Operator);
289 return None;
290 }
291
292 let mut close = None;
295 for at in *i + 1..self.bytes.len() {
296 if self.bytes[at] == b'$' {
297 close = Some(at);
298 break;
299 }
300 if !is_dollar_tag_char(self.bytes[at]) {
301 break;
302 }
303 }
304 let Some(close) = close else {
305 self.push(*i, *i + 1, Kind::Operator);
306 return None;
307 };
308
309 self.last = *i;
310 self.dollar_tag = Span::new(*i as u32 + 1, close as u32);
311 *i = close;
312 Some(State::DollarQuoted)
313 }
314
315 fn numeric(&mut self, state: &mut State, i: &mut usize, c: u8) {
316 if is_initial_number(c) {
317 return;
318 }
319 if c == b'_' && self.bytes.get(*i + 1).is_some_and(|&n| is_initial_number(n)) {
322 return;
323 }
324 if is_scientific(c) && !is_scientific(self.bytes[*i - 1]) {
325 if self.bytes[self.last].is_ascii_digit() || self.bytes[*i - 1].is_ascii_digit() {
328 return;
329 }
330 }
331 if (c == b'+' || c == b'-') && is_scientific(self.bytes[*i - 1]) {
332 return;
333 }
334
335 while !is_initial_number(self.bytes[*i - 1]) {
338 *i -= 1;
339 }
340 self.push(self.last, *i, Kind::Number);
341 *state = State::Standard;
342 self.last = *i;
343 *i -= 1;
344 }
345
346 fn operator(&mut self, state: &mut State, i: &mut usize, c: u8) {
347 if c == b'/' && self.bytes.get(*i + 1) == Some(&b'*') {
348 self.push_operator(self.last, *i);
349 *state = State::Standard;
350 self.last = *i;
351 *i -= 1;
352 return;
353 }
354 if !is_operator_char_in_run(c) {
355 self.push_operator(self.last, *i);
356 *state = State::Standard;
357 self.last = *i;
358 *i -= 1;
359 }
360 }
361
362 fn word(&mut self, state: &mut State, i: &mut usize, c: u8) {
363 if c == b'$' || is_word_char(c) {
366 return;
367 }
368 self.push_word(self.last, *i);
369 *state = State::Standard;
370 self.last = *i;
371 *i -= 1;
372 }
373
374 fn string_literal(&mut self, state: &mut State, i: &mut usize, c: u8) {
375 if self.escape_string && c == b'\\' && *i + 1 < self.bytes.len() {
376 *i += 1;
377 return;
378 }
379 if c != b'\'' {
380 return;
381 }
382 if self.bytes.get(*i + 1) == Some(&b'\'') {
383 *i += 1;
384 return;
385 }
386 self.push(self.last, *i + 1, Kind::String);
387 self.last = *i + 1;
388 self.escape_string = false;
389 *state = State::Standard;
390 }
391
392 fn quoted_identifier(&mut self, state: &mut State, i: &mut usize, c: u8) -> Result<()> {
393 if c != b'"' {
394 return Ok(());
395 }
396 if self.bytes.get(*i + 1) == Some(&b'"') {
397 *i += 1;
398 return Ok(());
399 }
400 if *i + 1 == self.last + 2 {
401 return Err(self.error("zero-length delimited identifier", self.last));
402 }
403 self.push(self.last, *i + 1, Kind::QuotedIdentifier);
404 self.last = *i + 1;
405 *state = State::Standard;
406 Ok(())
407 }
408
409 fn block_comment(&mut self, state: &mut State, i: &mut usize, c: u8) {
410 if c == b'/' && self.bytes.get(*i + 1) == Some(&b'*') {
413 *i += 1;
414 self.depth += 1;
415 } else if c == b'*' && self.bytes.get(*i + 1) == Some(&b'/') {
416 *i += 1;
417 self.depth -= 1;
418 if self.depth == 0 {
419 self.comment(self.last, *i + 1);
420 self.last = *i + 1;
421 *state = State::Standard;
422 }
423 }
424 }
425
426 fn dollar_quoted(&mut self, state: &mut State, i: &mut usize) {
427 if self.bytes[*i] != b'$' || *i + 1 >= self.bytes.len() {
428 return;
429 }
430 let start = *i + 1;
431 let mut end = start;
432 while end < self.bytes.len() && self.bytes[end] != b'$' {
433 end += 1;
434 }
435 if end >= self.bytes.len() {
436 return;
437 }
438 let tag = &self.bytes[self.dollar_tag.start as usize..self.dollar_tag.end as usize];
439 if end - start != tag.len() || &self.bytes[start..end] != tag {
440 return;
441 }
442 self.push(self.last, end + 1, Kind::String);
443 *state = State::Standard;
444 *i = end;
445 self.last = *i + 1;
446 }
447
448 fn push_word(&mut self, start: usize, end: usize) {
457 if start >= end {
458 return;
459 }
460 let keyword = lookup(&self.query[start..end]);
461 let kind = if classes(keyword) == 0 { Kind::Identifier } else { Kind::Keyword };
462 let flags = self.gap_flags(start);
463 self.tokens.push(Token { kind, flags, keyword, start: start as u32, end: end as u32 });
464 }
465
466 fn push_operator(&mut self, start: usize, end: usize) {
473 let special = self.bytes[start..end].iter().any(|&b| {
474 matches!(b, b'~' | b'!' | b'@' | b'#' | b'%' | b'^' | b'&' | b'|' | b'`' | b'?')
475 });
476 let mut cut = end;
477 if !special {
478 while cut > start && self.bytes[cut - 1] == b'+' {
479 cut -= 1;
480 }
481 }
482 self.push(start, cut, Kind::Operator);
483 for at in cut..end {
484 self.push(at, at + 1, Kind::Operator);
485 }
486 }
487
488 fn comment(&mut self, start: usize, end: usize) {
496 if end >= start + 2 && &self.bytes[start..start + 2] == b"/*" {
497 self.block_comment_at = Some(start);
498 if end >= start + 5 && self.bytes[start + 2] == b'+' {
499 self.hints.push(Span::new(start as u32 + 3, end as u32 - 2));
500 }
501 }
502 }
503
504 fn push(&mut self, start: usize, end: usize, kind: Kind) {
515 if start >= end {
516 return;
517 }
518 let flags = self.gap_flags(start);
519 self.tokens.push(Token {
520 kind,
521 flags,
522 keyword: NOT_A_KEYWORD,
523 start: start as u32,
524 end: end as u32,
525 });
526 }
527
528 fn push_flagged(&mut self, start: usize, end: usize, kind: Kind, extra: Flags) {
529 self.push(start, end, kind);
530 if let Some(token) = self.tokens.last_mut() {
531 token.flags = token.flags.with(extra);
532 }
533 }
534
535 fn gap_flags(&self, start: usize) -> Flags {
541 let Some(previous) = self.tokens.last() else { return Flags::default() };
542 let from = previous.end as usize;
543 let mut flags = Flags::default();
544 if self.block_comment_at.is_some_and(|at| at >= from && at < start) {
545 flags = flags.with(Flags::BLOCK_COMMENT);
546 }
547 if self.bytes[from..start.min(self.bytes.len())].iter().any(|&b| b == b'\n' || b == b'\r') {
548 flags = flags.with(Flags::NEWLINE);
549 }
550 flags
551 }
552
553 fn error(&self, message: impl Into<String>, at: usize) -> Error {
554 Error::parser(message).with_span(Span::new(at as u32, self.bytes.len() as u32))
555 }
556}
557
558pub fn lookup(word: &str) -> u16 {
563 if word.len() > LONGEST {
564 return NOT_A_KEYWORD;
565 }
566 let mut folded = [0u8; LONGEST];
567 for (slot, byte) in folded.iter_mut().zip(word.bytes()) {
568 *slot = byte.to_ascii_lowercase();
569 }
570 let folded = &folded[..word.len()];
571 match KEYWORDS.binary_search_by(|(candidate, _)| candidate.as_bytes().cmp(folded)) {
574 Ok(at) => at as u16,
575 Err(_) => NOT_A_KEYWORD,
576 }
577}
578
579pub fn classes(keyword: u16) -> u8 {
585 if keyword == NOT_A_KEYWORD { 0 } else { KEYWORDS[keyword as usize].1 }
586}
587
588const fn is_space(c: u8) -> bool {
589 matches!(c, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')
590}
591
592const fn is_single_byte_operator(c: u8) -> bool {
597 matches!(c, b'(' | b')' | b'{' | b'}' | b'[' | b']' | b',' | b'?' | b'$' | b'-' | b'#')
598}
599
600const fn is_operator_char(c: u8) -> bool {
602 if c == b'_' {
603 return false;
604 }
605 matches!(c, b'!'..=b'/' | b':'..=b'@' | b'['..=b'`' | b'{'..=b'~')
606}
607
608const fn is_operator_char_in_run(c: u8) -> bool {
614 if is_single_byte_operator(c) || is_control_flow(c) {
615 return false;
616 }
617 is_operator_char(c)
618}
619
620const fn is_control_flow(c: u8) -> bool {
621 matches!(c, b'\'' | b'-' | b';' | b'"' | b'.')
622}
623
624const fn is_word_char(c: u8) -> bool {
630 if is_single_byte_operator(c) || is_operator_char(c) || is_space(c) || is_control_flow(c) {
631 return false;
632 }
633 true
634}
635
636const fn is_initial_number(c: u8) -> bool {
641 c.is_ascii_digit() || c == b'.'
642}
643
644const fn is_scientific(c: u8) -> bool {
645 c == b'e' || c == b'E'
646}
647
648const fn is_string_prefix(c: u8) -> bool {
649 matches!(c, b'N' | b'n' | b'X' | b'x' | b'E' | b'e' | b'B' | b'b')
650}
651
652const fn is_dollar_tag_char(c: u8) -> bool {
655 c.is_ascii_alphanumeric() || c == b'_' || c >= 0x80
656}
657
658fn special_operator(bytes: &[u8], at: usize) -> Option<usize> {
663 if bytes[at..].starts_with(b"->>") {
664 return Some(3);
665 }
666 for candidate in [b"::".as_slice(), b":=", b"->", b"**", b"//"] {
667 if bytes[at..].starts_with(candidate) {
668 return Some(2);
669 }
670 }
671 None
672}
673
674#[cfg(test)]
675mod tests {
676 use super::{classes, lookup, tokenize};
677 use crate::generated::keywords::{RESERVED, UNRESERVED};
678 use crate::token::{Flags, Kind, NOT_A_KEYWORD, Token};
679
680 fn scan(query: &str) -> Vec<(Kind, &str)> {
682 let tokens = tokenize(query).expect("tokenizes");
683 assert_eq!(tokens.last().map(|t| t.kind), Some(Kind::EndOfInput));
684 assert_eq!(tokens.iter().filter(|t| t.kind == Kind::EndOfInput).count(), 1);
685 tokens[..tokens.len() - 1].iter().map(|t| (t.kind, t.text(query))).collect()
686 }
687
688 fn texts(query: &str) -> Vec<&str> {
689 scan(query).into_iter().map(|(_, text)| text).collect()
690 }
691
692 fn all(query: &str) -> Vec<Token> {
693 tokenize(query).expect("tokenizes")
694 }
695
696 fn message(query: &str) -> String {
697 tokenize(query).expect_err("fails").message().to_string()
698 }
699
700 #[test]
701 fn the_empty_query_is_one_sentinel() {
702 let tokens = tokenize("").expect("tokenizes");
703 assert_eq!(tokens.len(), 1);
704 assert_eq!(tokens[0].kind, Kind::EndOfInput);
705 assert_eq!(tokens[0].span(), rudb_common::Span::new(0, 0));
706 assert!(scan(" \t\n ").is_empty());
707 }
708
709 #[test]
710 fn a_word_in_a_class_is_a_keyword_and_one_in_none_is_not() {
711 assert_eq!(scan("SELECT"), [(Kind::Keyword, "SELECT")]);
712 assert_eq!(scan("banana"), [(Kind::Identifier, "banana")]);
713 }
714
715 #[test]
716 fn case_is_matched_but_not_folded() {
717 for spelling in ["select", "SELECT", "SeLeCt"] {
721 let tokens = all(spelling);
722 assert_eq!(tokens[0].kind, Kind::Keyword);
723 assert_eq!(tokens[0].text(spelling), spelling);
724 assert_eq!(tokens[0].keyword, lookup("select"));
725 }
726 assert_eq!(scan(r#""Foo""#), [(Kind::QuotedIdentifier, r#""Foo""#)]);
727 }
728
729 #[test]
730 fn a_soft_word_keeps_its_index_and_stays_an_identifier() {
731 let tokens = all("ascending");
735 assert_eq!(tokens[0].kind, Kind::Identifier);
736 assert_ne!(tokens[0].keyword, NOT_A_KEYWORD);
737 assert_eq!(classes(tokens[0].keyword), 0);
738
739 let tokens = all("banana");
740 assert_eq!(tokens[0].keyword, NOT_A_KEYWORD);
741 assert_eq!(classes(tokens[0].keyword), 0);
742 }
743
744 #[test]
745 fn the_classes_come_back_off_the_index() {
746 assert_eq!(classes(lookup("select")) & RESERVED, RESERVED);
747 assert_eq!(classes(lookup("abort")) & UNRESERVED, UNRESERVED);
748 assert_eq!(lookup("supercalifragilistic"), NOT_A_KEYWORD);
749 assert_eq!(lookup(""), NOT_A_KEYWORD);
750 }
751
752 #[test]
753 fn a_quoted_identifier_is_never_a_keyword() {
754 let tokens = all(r#""select""#);
755 assert_eq!(tokens[0].kind, Kind::QuotedIdentifier);
756 assert_eq!(tokens[0].keyword, NOT_A_KEYWORD);
757 assert_eq!(scan(r#""a""b""#), [(Kind::QuotedIdentifier, r#""a""b""#)]);
758 }
759
760 #[test]
761 fn a_dollar_is_an_identifier_character_after_the_first_byte() {
762 assert_eq!(scan("a$b"), [(Kind::Identifier, "a$b")]);
763 }
764
765 #[test]
766 fn any_byte_above_ascii_is_an_identifier_character() {
767 assert_eq!(scan("SELECT café"), [(Kind::Keyword, "SELECT"), (Kind::Identifier, "café")]);
771 }
772
773 #[test]
774 fn a_number_swallows_more_than_a_number() {
775 assert_eq!(scan("1.2.3"), [(Kind::Number, "1.2.3")]);
778 assert_eq!(scan("1_000"), [(Kind::Number, "1_000")]);
779 assert_eq!(scan("1e5"), [(Kind::Number, "1e5")]);
780 assert_eq!(scan(".1e5"), [(Kind::Number, ".1e5")]);
781 assert_eq!(scan("1e-5"), [(Kind::Number, "1e-5")]);
782 assert_eq!(scan("1.e5"), [(Kind::Number, "1.e5")]);
783 }
784
785 #[test]
786 fn a_trailing_e_stays_on_the_number() {
787 assert_eq!(scan("SELECT 1e"), [(Kind::Keyword, "SELECT"), (Kind::Number, "1e")]);
790 assert_eq!(scan("SELECT 1e+"), [(Kind::Keyword, "SELECT"), (Kind::Number, "1e+")]);
791 }
792
793 #[test]
794 fn what_the_number_cannot_use_it_gives_back() {
795 assert_eq!(
798 scan("SELECT 1e+ 1"),
799 [
800 (Kind::Keyword, "SELECT"),
801 (Kind::Number, "1"),
802 (Kind::Identifier, "e"),
803 (Kind::Operator, "+"),
804 (Kind::Number, "1"),
805 ]
806 );
807 assert_eq!(scan("1_"), [(Kind::Number, "1"), (Kind::Identifier, "_")]);
808 assert_eq!(scan("1__0"), [(Kind::Number, "1"), (Kind::Identifier, "__0")]);
809 assert_eq!(scan("0x1F"), [(Kind::Number, "0"), (Kind::Identifier, "x1F")]);
812 assert_eq!(scan(".e100"), [(Kind::Number, "."), (Kind::Identifier, "e100")]);
813 }
814
815 #[test]
816 fn a_minus_never_joins_an_operator_run() {
817 assert_eq!(texts("1-1"), ["1", "-", "1"]);
820 assert_eq!(texts("SELECT 1 =- 1"), ["SELECT", "1", "=", "-", "1"]);
821 assert_eq!(texts("(a,b)"), ["(", "a", ",", "b", ")"]);
822 }
823
824 #[test]
825 fn the_postgres_plus_rule_decides_where_a_run_ends() {
826 assert_eq!(texts("SELECT 1 =+ 1"), ["SELECT", "1", "=", "+", "1"]);
829 assert_eq!(texts("SELECT 1 !=+ 1"), ["SELECT", "1", "!=+", "1"]);
830 assert_eq!(texts("SELECT 1 =++ 1"), ["SELECT", "1", "=", "+", "+", "1"]);
831 assert_eq!(texts("SELECT 1 ++ 1"), ["SELECT", "1", "+", "+", "1"]);
832 }
833
834 #[test]
835 fn the_special_operators_are_checked_before_the_run() {
836 assert_eq!(texts("a->>'b'"), ["a", "->>", "'b'"]);
837 assert_eq!(texts("a->'b'"), ["a", "->", "'b'"]);
838 assert_eq!(texts("a::b"), ["a", "::", "b"]);
839 assert_eq!(texts("a//b"), ["a", "//", "b"]);
840 assert_eq!(texts("2**3"), ["2", "**", "3"]);
841 assert_eq!(texts("a::=b"), ["a", "::=", "b"]);
844 }
845
846 #[test]
847 fn a_block_comment_can_end_an_operator_run() {
848 assert_eq!(texts("1+/*c*/2"), ["1", "+", "2"]);
849 }
850
851 #[test]
852 fn a_comment_is_not_a_token_but_a_block_one_leaves_a_mark() {
853 assert_eq!(texts("SELECT --x\n1"), ["SELECT", "1"]);
854 assert_eq!(texts("SELECT /*x*/ 1"), ["SELECT", "1"]);
855 assert_eq!(texts("SELECT --x"), ["SELECT"]);
856
857 let tokens = all("SELECT /*x*/ 1");
858 assert!(tokens[1].flags.has(Flags::BLOCK_COMMENT));
859 assert!(!tokens[1].flags.has(Flags::NEWLINE));
860
861 let tokens = all("SELECT --x\n1");
863 assert!(!tokens[1].flags.has(Flags::BLOCK_COMMENT));
864 assert!(tokens[1].flags.has(Flags::NEWLINE));
865 }
866
867 #[test]
868 fn a_hint_is_a_comment_that_can_be_read_back() {
869 assert_eq!(
870 super::hints("SELECT /*+ hash.table(unchained) */ 1").unwrap(),
871 [" hash.table(unchained) "]
872 );
873 assert_eq!(texts("SELECT /*+ hash.table(unchained) */ 1"), ["SELECT", "1"]);
875
876 assert_eq!(super::hints("SELECT /*+ a(b) */ 1 /*+ c(d) */").unwrap(), [" a(b) ", " c(d) "]);
878
879 assert!(super::hints("SELECT /* hash.table(unchained) */ 1").unwrap().is_empty());
882 assert!(super::hints("SELECT '/*+ hash.table(unchained) */'").unwrap().is_empty());
883 assert!(super::hints("SELECT 1").unwrap().is_empty());
884 }
885
886 #[test]
887 fn the_first_token_is_preceded_by_nothing() {
888 let tokens = all("\n/*x*/ SELECT");
889 assert_eq!(tokens[0].flags, Flags::default());
890 }
891
892 #[test]
893 fn block_comments_nest() {
894 assert_eq!(texts("SELECT /* a /* b */ c */ 1"), ["SELECT", "1"]);
897 assert_eq!(
898 message("SELECT /* a /* b */ 1"),
899 "unterminated /* comment at or near \"/* a /* b */ 1\""
900 );
901 }
902
903 #[test]
904 fn a_string_keeps_its_quotes_and_its_escapes() {
905 assert_eq!(scan("'it''s'"), [(Kind::String, "'it''s'")]);
906 assert_eq!(scan("''"), [(Kind::String, "''")]);
907 assert_eq!(texts("'a' 'b'"), ["'a'", "'b'"]);
908 }
909
910 #[test]
911 fn only_the_e_prefix_changes_how_a_string_is_read() {
912 assert_eq!(scan(r"E'\''"), [(Kind::String, r"E'\''")]);
916 assert_eq!(message(r"'\''"), "unterminated string literal");
917 for prefix in ["X", "x", "B", "b", "N", "n", "E", "e"] {
918 let query = format!("{prefix}'a'");
919 assert_eq!(tokenize(&query).expect("tokenizes")[0].kind, Kind::String);
920 }
921 assert_eq!(texts("x 'a'"), ["x", "'a'"]);
923 }
924
925 #[test]
926 fn a_dollar_quoted_string_is_one_token_and_its_tag_has_to_match() {
927 assert_eq!(scan("$$abc$$"), [(Kind::String, "$$abc$$")]);
928 assert_eq!(scan("$tag$abc$tag$"), [(Kind::String, "$tag$abc$tag$")]);
929 assert_eq!(scan("$tag$a$other$b$tag$"), [(Kind::String, "$tag$a$other$b$tag$")]);
930 assert_eq!(scan("$$it's fine$$"), [(Kind::String, "$$it's fine$$")]);
931 }
932
933 #[test]
934 fn an_unterminated_dollar_quote_is_a_token_and_not_an_error() {
935 let tokens = all("$$abc");
938 assert_eq!(tokens[0].kind, Kind::String);
939 assert!(tokens[0].flags.has(Flags::UNTERMINATED));
940 assert_eq!(tokens[0].text("$$abc"), "$$abc");
941 }
942
943 #[test]
944 fn a_parameter_is_two_tokens() {
945 assert_eq!(scan("$1"), [(Kind::Operator, "$"), (Kind::Number, "1")]);
948 assert_eq!(scan("$banana"), [(Kind::Operator, "$"), (Kind::Identifier, "banana")]);
949 assert_eq!(scan("?"), [(Kind::Operator, "?")]);
950 assert_eq!(scan("$"), [(Kind::Identifier, "$")]);
952 }
953
954 #[test]
955 fn a_semicolon_is_its_own_kind() {
956 assert_eq!(
957 scan("SELECT 1; SELECT 2"),
958 [
959 (Kind::Keyword, "SELECT"),
960 (Kind::Number, "1"),
961 (Kind::Terminator, ";"),
962 (Kind::Keyword, "SELECT"),
963 (Kind::Number, "2"),
964 ]
965 );
966 assert_eq!(scan(";"), [(Kind::Terminator, ";")]);
967 }
968
969 #[test]
970 fn the_four_errors_are_the_four_upstream_throws() {
971 assert_eq!(message("SELECT /* x"), "unterminated /* comment at or near \"/* x\"");
972 assert_eq!(message("SELECT 'x"), "unterminated string literal");
973 assert_eq!(message("SELECT \"x"), "unterminated quoted identifier");
974 assert_eq!(message("SELECT \"\""), "zero-length delimited identifier");
975 assert_eq!(tokenize("SELECT 'x").expect_err("fails").span().map(|s| s.start), Some(7));
977 }
978
979 #[test]
980 fn every_span_lands_where_the_text_is() {
981 let query = "SELECT a, /*c*/ 'b' || $$d$$ FROM t;";
982 for token in tokenize(query).expect("tokenizes") {
983 assert!(token.end as usize <= query.len());
984 assert!(token.start <= token.end);
985 if token.kind != Kind::EndOfInput {
986 assert!(!token.text(query).is_empty());
987 }
988 }
989 }
990
991 #[test]
992 fn a_real_query_comes_out_the_way_it_reads() {
993 assert_eq!(
994 scan("SELECT count(*) FROM t WHERE x > 5 AND y::VARCHAR = 'a';"),
995 [
996 (Kind::Keyword, "SELECT"),
997 (Kind::Identifier, "count"),
998 (Kind::Operator, "("),
999 (Kind::Operator, "*"),
1000 (Kind::Operator, ")"),
1001 (Kind::Keyword, "FROM"),
1002 (Kind::Identifier, "t"),
1003 (Kind::Keyword, "WHERE"),
1004 (Kind::Identifier, "x"),
1005 (Kind::Operator, ">"),
1006 (Kind::Number, "5"),
1007 (Kind::Keyword, "AND"),
1008 (Kind::Identifier, "y"),
1009 (Kind::Operator, "::"),
1010 (Kind::Keyword, "VARCHAR"),
1011 (Kind::Operator, "="),
1012 (Kind::String, "'a'"),
1013 (Kind::Terminator, ";"),
1014 ]
1015 );
1016 }
1017}