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 Tokenizer::new(query).run()
38}
39
40#[derive(Clone, Copy, PartialEq, Eq)]
45enum State {
46 Standard,
47 LineComment,
48 BlockComment,
49 QuotedIdentifier,
50 StringLiteral,
51 Word,
52 Numeric,
53 Operator,
54 DollarQuoted,
55}
56
57struct Tokenizer<'a> {
58 query: &'a str,
59 bytes: &'a [u8],
60 tokens: Vec<Token>,
61 last: usize,
63 block_comment_at: Option<usize>,
66 escape_string: bool,
68 dollar_tag: Span,
70 depth: u32,
71}
72
73impl<'a> Tokenizer<'a> {
74 fn new(query: &'a str) -> Self {
75 Tokenizer {
76 query,
77 bytes: query.as_bytes(),
78 tokens: Vec::with_capacity(query.len() / 4 + 4),
81 last: 0,
82 block_comment_at: None,
83 escape_string: false,
84 dollar_tag: Span::new(0, 0),
85 depth: 0,
86 }
87 }
88
89 fn run(mut self) -> Result<Vec<Token>> {
90 let mut state = State::Standard;
91 let mut i = 0;
92 while i < self.bytes.len() {
93 let c = self.bytes[i];
94 match state {
95 State::Standard => {
96 if let Some(next) = self.standard(&mut i, c)? {
97 state = next;
98 }
99 }
100 State::Numeric => self.numeric(&mut state, &mut i, c),
101 State::Operator => self.operator(&mut state, &mut i, c),
102 State::Word => self.word(&mut state, &mut i, c),
103 State::StringLiteral => self.string_literal(&mut state, &mut i, c),
104 State::QuotedIdentifier => self.quoted_identifier(&mut state, &mut i, c)?,
105 State::LineComment => {
106 if c == b'\n' || c == b'\r' {
107 self.comment(self.last, i + 1);
108 self.last = i + 1;
109 state = State::Standard;
110 }
111 }
112 State::BlockComment => self.block_comment(&mut state, &mut i, c),
113 State::DollarQuoted => self.dollar_quoted(&mut state, &mut i),
114 }
115 i += 1;
116 }
117 self.finish(state)
118 }
119
120 fn finish(mut self, state: State) -> Result<Vec<Token>> {
122 let end = self.bytes.len();
123 match state {
124 State::LineComment => {
125 self.comment(self.last, end);
126 }
127 State::BlockComment => {
128 return Err(self.error(
129 format!(
130 "unterminated /* comment at or near \"{}\"",
131 &self.query[self.last..end]
132 ),
133 self.last,
134 ));
135 }
136 State::Operator => self.push_operator(self.last, end),
137 State::DollarQuoted => {
138 self.push_flagged(self.last, end, Kind::String, Flags::UNTERMINATED);
141 }
142 State::StringLiteral => {
143 return Err(self.error("unterminated string literal", self.last));
144 }
145 State::QuotedIdentifier => {
146 return Err(self.error("unterminated quoted identifier", self.last));
147 }
148 State::Numeric => self.push(self.last, end, Kind::Number),
149 State::Word => self.push_word(self.last, end),
150 State::Standard => self.push(self.last, end, Kind::Identifier),
154 }
155 self.tokens.push(Token {
156 kind: Kind::EndOfInput,
157 flags: Flags::default(),
158 keyword: NOT_A_KEYWORD,
159 start: end as u32,
160 end: end as u32,
161 });
162 Ok(self.tokens)
163 }
164
165 fn standard(&mut self, i: &mut usize, c: u8) -> Result<Option<State>> {
167 match c {
168 b'\'' => {
169 self.last = *i;
170 self.escape_string = false;
171 return Ok(Some(State::StringLiteral));
172 }
173 b'"' => {
174 self.last = *i;
175 return Ok(Some(State::QuotedIdentifier));
176 }
177 b';' => {
178 self.tokens.push(Token {
182 kind: Kind::Terminator,
183 flags: self.gap_flags(*i),
184 keyword: NOT_A_KEYWORD,
185 start: *i as u32,
186 end: *i as u32 + 1,
187 });
188 self.last = *i + 1;
189 return Ok(None);
190 }
191 b'$' => return Ok(self.dollar(i)),
192 b'-' if self.bytes.get(*i + 1) == Some(&b'-') => {
193 *i += 1;
194 return Ok(Some(State::LineComment));
195 }
196 b'/' if self.bytes.get(*i + 1) == Some(&b'*') => {
197 *i += 1;
198 self.depth = 1;
199 return Ok(Some(State::BlockComment));
200 }
201 _ => {}
202 }
203
204 if is_space(c) {
205 self.last = *i + 1;
206 return Ok(None);
207 }
208
209 if let Some(len) = special_operator(self.bytes, *i) {
210 if self.bytes.get(*i + len).is_some_and(|&next| is_operator_char_in_run(next)) {
213 self.last = *i;
214 return Ok(Some(State::Operator));
215 }
216 self.push(*i, *i + len, Kind::Operator);
217 *i += len - 1;
218 self.last = *i + 1;
219 return Ok(None);
220 }
221
222 if is_single_byte_operator(c) {
223 self.push(*i, *i + 1, Kind::Operator);
224 self.last = *i + 1;
225 return Ok(None);
226 }
227
228 if is_initial_number(c) {
229 self.last = *i;
230 return Ok(Some(State::Numeric));
231 }
232
233 if is_string_prefix(c) && self.bytes.get(*i + 1) == Some(&b'\'') {
237 self.last = *i;
238 self.escape_string = c == b'E' || c == b'e';
239 *i += 1;
240 return Ok(Some(State::StringLiteral));
241 }
242
243 if is_operator_char(c) {
244 self.last = *i;
245 return Ok(Some(State::Operator));
246 }
247
248 self.last = *i;
249 Ok(Some(State::Word))
250 }
251
252 fn dollar(&mut self, i: &mut usize) -> Option<State> {
254 let Some(&next) = self.bytes.get(*i + 1) else {
255 return None;
258 };
259 if next.is_ascii_digit() {
260 self.push(*i, *i + 1, Kind::Operator);
263 return None;
264 }
265
266 let mut close = None;
269 for at in *i + 1..self.bytes.len() {
270 if self.bytes[at] == b'$' {
271 close = Some(at);
272 break;
273 }
274 if !is_dollar_tag_char(self.bytes[at]) {
275 break;
276 }
277 }
278 let Some(close) = close else {
279 self.push(*i, *i + 1, Kind::Operator);
280 return None;
281 };
282
283 self.last = *i;
284 self.dollar_tag = Span::new(*i as u32 + 1, close as u32);
285 *i = close;
286 Some(State::DollarQuoted)
287 }
288
289 fn numeric(&mut self, state: &mut State, i: &mut usize, c: u8) {
290 if is_initial_number(c) {
291 return;
292 }
293 if c == b'_' && self.bytes.get(*i + 1).is_some_and(|&n| is_initial_number(n)) {
296 return;
297 }
298 if is_scientific(c) && !is_scientific(self.bytes[*i - 1]) {
299 if self.bytes[self.last].is_ascii_digit() || self.bytes[*i - 1].is_ascii_digit() {
302 return;
303 }
304 }
305 if (c == b'+' || c == b'-') && is_scientific(self.bytes[*i - 1]) {
306 return;
307 }
308
309 while !is_initial_number(self.bytes[*i - 1]) {
312 *i -= 1;
313 }
314 self.push(self.last, *i, Kind::Number);
315 *state = State::Standard;
316 self.last = *i;
317 *i -= 1;
318 }
319
320 fn operator(&mut self, state: &mut State, i: &mut usize, c: u8) {
321 if c == b'/' && self.bytes.get(*i + 1) == Some(&b'*') {
322 self.push_operator(self.last, *i);
323 *state = State::Standard;
324 self.last = *i;
325 *i -= 1;
326 return;
327 }
328 if !is_operator_char_in_run(c) {
329 self.push_operator(self.last, *i);
330 *state = State::Standard;
331 self.last = *i;
332 *i -= 1;
333 }
334 }
335
336 fn word(&mut self, state: &mut State, i: &mut usize, c: u8) {
337 if c == b'$' || is_word_char(c) {
340 return;
341 }
342 self.push_word(self.last, *i);
343 *state = State::Standard;
344 self.last = *i;
345 *i -= 1;
346 }
347
348 fn string_literal(&mut self, state: &mut State, i: &mut usize, c: u8) {
349 if self.escape_string && c == b'\\' && *i + 1 < self.bytes.len() {
350 *i += 1;
351 return;
352 }
353 if c != b'\'' {
354 return;
355 }
356 if self.bytes.get(*i + 1) == Some(&b'\'') {
357 *i += 1;
358 return;
359 }
360 self.push(self.last, *i + 1, Kind::String);
361 self.last = *i + 1;
362 self.escape_string = false;
363 *state = State::Standard;
364 }
365
366 fn quoted_identifier(&mut self, state: &mut State, i: &mut usize, c: u8) -> Result<()> {
367 if c != b'"' {
368 return Ok(());
369 }
370 if self.bytes.get(*i + 1) == Some(&b'"') {
371 *i += 1;
372 return Ok(());
373 }
374 if *i + 1 == self.last + 2 {
375 return Err(self.error("zero-length delimited identifier", self.last));
376 }
377 self.push(self.last, *i + 1, Kind::QuotedIdentifier);
378 self.last = *i + 1;
379 *state = State::Standard;
380 Ok(())
381 }
382
383 fn block_comment(&mut self, state: &mut State, i: &mut usize, c: u8) {
384 if c == b'/' && self.bytes.get(*i + 1) == Some(&b'*') {
387 *i += 1;
388 self.depth += 1;
389 } else if c == b'*' && self.bytes.get(*i + 1) == Some(&b'/') {
390 *i += 1;
391 self.depth -= 1;
392 if self.depth == 0 {
393 self.comment(self.last, *i + 1);
394 self.last = *i + 1;
395 *state = State::Standard;
396 }
397 }
398 }
399
400 fn dollar_quoted(&mut self, state: &mut State, i: &mut usize) {
401 if self.bytes[*i] != b'$' || *i + 1 >= self.bytes.len() {
402 return;
403 }
404 let start = *i + 1;
405 let mut end = start;
406 while end < self.bytes.len() && self.bytes[end] != b'$' {
407 end += 1;
408 }
409 if end >= self.bytes.len() {
410 return;
411 }
412 let tag = &self.bytes[self.dollar_tag.start as usize..self.dollar_tag.end as usize];
413 if end - start != tag.len() || &self.bytes[start..end] != tag {
414 return;
415 }
416 self.push(self.last, end + 1, Kind::String);
417 *state = State::Standard;
418 *i = end;
419 self.last = *i + 1;
420 }
421
422 fn push_word(&mut self, start: usize, end: usize) {
431 if start >= end {
432 return;
433 }
434 let keyword = lookup(&self.query[start..end]);
435 let kind = if classes(keyword) == 0 { Kind::Identifier } else { Kind::Keyword };
436 let flags = self.gap_flags(start);
437 self.tokens.push(Token { kind, flags, keyword, start: start as u32, end: end as u32 });
438 }
439
440 fn push_operator(&mut self, start: usize, end: usize) {
447 let special = self.bytes[start..end].iter().any(|&b| {
448 matches!(b, b'~' | b'!' | b'@' | b'#' | b'%' | b'^' | b'&' | b'|' | b'`' | b'?')
449 });
450 let mut cut = end;
451 if !special {
452 while cut > start && self.bytes[cut - 1] == b'+' {
453 cut -= 1;
454 }
455 }
456 self.push(start, cut, Kind::Operator);
457 for at in cut..end {
458 self.push(at, at + 1, Kind::Operator);
459 }
460 }
461
462 fn comment(&mut self, start: usize, end: usize) {
466 if end >= start + 2 && &self.bytes[start..start + 2] == b"/*" {
467 self.block_comment_at = Some(start);
468 }
469 }
470
471 fn push(&mut self, start: usize, end: usize, kind: Kind) {
482 if start >= end {
483 return;
484 }
485 let flags = self.gap_flags(start);
486 self.tokens.push(Token {
487 kind,
488 flags,
489 keyword: NOT_A_KEYWORD,
490 start: start as u32,
491 end: end as u32,
492 });
493 }
494
495 fn push_flagged(&mut self, start: usize, end: usize, kind: Kind, extra: Flags) {
496 self.push(start, end, kind);
497 if let Some(token) = self.tokens.last_mut() {
498 token.flags = token.flags.with(extra);
499 }
500 }
501
502 fn gap_flags(&self, start: usize) -> Flags {
508 let Some(previous) = self.tokens.last() else { return Flags::default() };
509 let from = previous.end as usize;
510 let mut flags = Flags::default();
511 if self.block_comment_at.is_some_and(|at| at >= from && at < start) {
512 flags = flags.with(Flags::BLOCK_COMMENT);
513 }
514 if self.bytes[from..start.min(self.bytes.len())].iter().any(|&b| b == b'\n' || b == b'\r') {
515 flags = flags.with(Flags::NEWLINE);
516 }
517 flags
518 }
519
520 fn error(&self, message: impl Into<String>, at: usize) -> Error {
521 Error::parser(message).with_span(Span::new(at as u32, self.bytes.len() as u32))
522 }
523}
524
525pub fn lookup(word: &str) -> u16 {
530 if word.len() > LONGEST {
531 return NOT_A_KEYWORD;
532 }
533 let mut folded = [0u8; LONGEST];
534 for (slot, byte) in folded.iter_mut().zip(word.bytes()) {
535 *slot = byte.to_ascii_lowercase();
536 }
537 let folded = &folded[..word.len()];
538 match KEYWORDS.binary_search_by(|(candidate, _)| candidate.as_bytes().cmp(folded)) {
541 Ok(at) => at as u16,
542 Err(_) => NOT_A_KEYWORD,
543 }
544}
545
546pub fn classes(keyword: u16) -> u8 {
552 if keyword == NOT_A_KEYWORD { 0 } else { KEYWORDS[keyword as usize].1 }
553}
554
555const fn is_space(c: u8) -> bool {
556 matches!(c, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')
557}
558
559const fn is_single_byte_operator(c: u8) -> bool {
564 matches!(c, b'(' | b')' | b'{' | b'}' | b'[' | b']' | b',' | b'?' | b'$' | b'-' | b'#')
565}
566
567const fn is_operator_char(c: u8) -> bool {
569 if c == b'_' {
570 return false;
571 }
572 matches!(c, b'!'..=b'/' | b':'..=b'@' | b'['..=b'`' | b'{'..=b'~')
573}
574
575const fn is_operator_char_in_run(c: u8) -> bool {
581 if is_single_byte_operator(c) || is_control_flow(c) {
582 return false;
583 }
584 is_operator_char(c)
585}
586
587const fn is_control_flow(c: u8) -> bool {
588 matches!(c, b'\'' | b'-' | b';' | b'"' | b'.')
589}
590
591const fn is_word_char(c: u8) -> bool {
597 if is_single_byte_operator(c) || is_operator_char(c) || is_space(c) || is_control_flow(c) {
598 return false;
599 }
600 true
601}
602
603const fn is_initial_number(c: u8) -> bool {
608 c.is_ascii_digit() || c == b'.'
609}
610
611const fn is_scientific(c: u8) -> bool {
612 c == b'e' || c == b'E'
613}
614
615const fn is_string_prefix(c: u8) -> bool {
616 matches!(c, b'N' | b'n' | b'X' | b'x' | b'E' | b'e' | b'B' | b'b')
617}
618
619const fn is_dollar_tag_char(c: u8) -> bool {
622 c.is_ascii_alphanumeric() || c == b'_' || c >= 0x80
623}
624
625fn special_operator(bytes: &[u8], at: usize) -> Option<usize> {
630 if bytes[at..].starts_with(b"->>") {
631 return Some(3);
632 }
633 for candidate in [b"::".as_slice(), b":=", b"->", b"**", b"//"] {
634 if bytes[at..].starts_with(candidate) {
635 return Some(2);
636 }
637 }
638 None
639}
640
641#[cfg(test)]
642mod tests {
643 use super::{classes, lookup, tokenize};
644 use crate::generated::keywords::{RESERVED, UNRESERVED};
645 use crate::token::{Flags, Kind, NOT_A_KEYWORD, Token};
646
647 fn scan(query: &str) -> Vec<(Kind, &str)> {
649 let tokens = tokenize(query).expect("tokenizes");
650 assert_eq!(tokens.last().map(|t| t.kind), Some(Kind::EndOfInput));
651 assert_eq!(tokens.iter().filter(|t| t.kind == Kind::EndOfInput).count(), 1);
652 tokens[..tokens.len() - 1].iter().map(|t| (t.kind, t.text(query))).collect()
653 }
654
655 fn texts(query: &str) -> Vec<&str> {
656 scan(query).into_iter().map(|(_, text)| text).collect()
657 }
658
659 fn all(query: &str) -> Vec<Token> {
660 tokenize(query).expect("tokenizes")
661 }
662
663 fn message(query: &str) -> String {
664 tokenize(query).expect_err("fails").message().to_string()
665 }
666
667 #[test]
668 fn the_empty_query_is_one_sentinel() {
669 let tokens = tokenize("").expect("tokenizes");
670 assert_eq!(tokens.len(), 1);
671 assert_eq!(tokens[0].kind, Kind::EndOfInput);
672 assert_eq!(tokens[0].span(), rudb_common::Span::new(0, 0));
673 assert!(scan(" \t\n ").is_empty());
674 }
675
676 #[test]
677 fn a_word_in_a_class_is_a_keyword_and_one_in_none_is_not() {
678 assert_eq!(scan("SELECT"), [(Kind::Keyword, "SELECT")]);
679 assert_eq!(scan("banana"), [(Kind::Identifier, "banana")]);
680 }
681
682 #[test]
683 fn case_is_matched_but_not_folded() {
684 for spelling in ["select", "SELECT", "SeLeCt"] {
688 let tokens = all(spelling);
689 assert_eq!(tokens[0].kind, Kind::Keyword);
690 assert_eq!(tokens[0].text(spelling), spelling);
691 assert_eq!(tokens[0].keyword, lookup("select"));
692 }
693 assert_eq!(scan(r#""Foo""#), [(Kind::QuotedIdentifier, r#""Foo""#)]);
694 }
695
696 #[test]
697 fn a_soft_word_keeps_its_index_and_stays_an_identifier() {
698 let tokens = all("ascending");
702 assert_eq!(tokens[0].kind, Kind::Identifier);
703 assert_ne!(tokens[0].keyword, NOT_A_KEYWORD);
704 assert_eq!(classes(tokens[0].keyword), 0);
705
706 let tokens = all("banana");
707 assert_eq!(tokens[0].keyword, NOT_A_KEYWORD);
708 assert_eq!(classes(tokens[0].keyword), 0);
709 }
710
711 #[test]
712 fn the_classes_come_back_off_the_index() {
713 assert_eq!(classes(lookup("select")) & RESERVED, RESERVED);
714 assert_eq!(classes(lookup("abort")) & UNRESERVED, UNRESERVED);
715 assert_eq!(lookup("supercalifragilistic"), NOT_A_KEYWORD);
716 assert_eq!(lookup(""), NOT_A_KEYWORD);
717 }
718
719 #[test]
720 fn a_quoted_identifier_is_never_a_keyword() {
721 let tokens = all(r#""select""#);
722 assert_eq!(tokens[0].kind, Kind::QuotedIdentifier);
723 assert_eq!(tokens[0].keyword, NOT_A_KEYWORD);
724 assert_eq!(scan(r#""a""b""#), [(Kind::QuotedIdentifier, r#""a""b""#)]);
725 }
726
727 #[test]
728 fn a_dollar_is_an_identifier_character_after_the_first_byte() {
729 assert_eq!(scan("a$b"), [(Kind::Identifier, "a$b")]);
730 }
731
732 #[test]
733 fn any_byte_above_ascii_is_an_identifier_character() {
734 assert_eq!(scan("SELECT café"), [(Kind::Keyword, "SELECT"), (Kind::Identifier, "café")]);
738 }
739
740 #[test]
741 fn a_number_swallows_more_than_a_number() {
742 assert_eq!(scan("1.2.3"), [(Kind::Number, "1.2.3")]);
745 assert_eq!(scan("1_000"), [(Kind::Number, "1_000")]);
746 assert_eq!(scan("1e5"), [(Kind::Number, "1e5")]);
747 assert_eq!(scan(".1e5"), [(Kind::Number, ".1e5")]);
748 assert_eq!(scan("1e-5"), [(Kind::Number, "1e-5")]);
749 assert_eq!(scan("1.e5"), [(Kind::Number, "1.e5")]);
750 }
751
752 #[test]
753 fn a_trailing_e_stays_on_the_number() {
754 assert_eq!(scan("SELECT 1e"), [(Kind::Keyword, "SELECT"), (Kind::Number, "1e")]);
757 assert_eq!(scan("SELECT 1e+"), [(Kind::Keyword, "SELECT"), (Kind::Number, "1e+")]);
758 }
759
760 #[test]
761 fn what_the_number_cannot_use_it_gives_back() {
762 assert_eq!(
765 scan("SELECT 1e+ 1"),
766 [
767 (Kind::Keyword, "SELECT"),
768 (Kind::Number, "1"),
769 (Kind::Identifier, "e"),
770 (Kind::Operator, "+"),
771 (Kind::Number, "1"),
772 ]
773 );
774 assert_eq!(scan("1_"), [(Kind::Number, "1"), (Kind::Identifier, "_")]);
775 assert_eq!(scan("1__0"), [(Kind::Number, "1"), (Kind::Identifier, "__0")]);
776 assert_eq!(scan("0x1F"), [(Kind::Number, "0"), (Kind::Identifier, "x1F")]);
779 assert_eq!(scan(".e100"), [(Kind::Number, "."), (Kind::Identifier, "e100")]);
780 }
781
782 #[test]
783 fn a_minus_never_joins_an_operator_run() {
784 assert_eq!(texts("1-1"), ["1", "-", "1"]);
787 assert_eq!(texts("SELECT 1 =- 1"), ["SELECT", "1", "=", "-", "1"]);
788 assert_eq!(texts("(a,b)"), ["(", "a", ",", "b", ")"]);
789 }
790
791 #[test]
792 fn the_postgres_plus_rule_decides_where_a_run_ends() {
793 assert_eq!(texts("SELECT 1 =+ 1"), ["SELECT", "1", "=", "+", "1"]);
796 assert_eq!(texts("SELECT 1 !=+ 1"), ["SELECT", "1", "!=+", "1"]);
797 assert_eq!(texts("SELECT 1 =++ 1"), ["SELECT", "1", "=", "+", "+", "1"]);
798 assert_eq!(texts("SELECT 1 ++ 1"), ["SELECT", "1", "+", "+", "1"]);
799 }
800
801 #[test]
802 fn the_special_operators_are_checked_before_the_run() {
803 assert_eq!(texts("a->>'b'"), ["a", "->>", "'b'"]);
804 assert_eq!(texts("a->'b'"), ["a", "->", "'b'"]);
805 assert_eq!(texts("a::b"), ["a", "::", "b"]);
806 assert_eq!(texts("a//b"), ["a", "//", "b"]);
807 assert_eq!(texts("2**3"), ["2", "**", "3"]);
808 assert_eq!(texts("a::=b"), ["a", "::=", "b"]);
811 }
812
813 #[test]
814 fn a_block_comment_can_end_an_operator_run() {
815 assert_eq!(texts("1+/*c*/2"), ["1", "+", "2"]);
816 }
817
818 #[test]
819 fn a_comment_is_not_a_token_but_a_block_one_leaves_a_mark() {
820 assert_eq!(texts("SELECT --x\n1"), ["SELECT", "1"]);
821 assert_eq!(texts("SELECT /*x*/ 1"), ["SELECT", "1"]);
822 assert_eq!(texts("SELECT --x"), ["SELECT"]);
823
824 let tokens = all("SELECT /*x*/ 1");
825 assert!(tokens[1].flags.has(Flags::BLOCK_COMMENT));
826 assert!(!tokens[1].flags.has(Flags::NEWLINE));
827
828 let tokens = all("SELECT --x\n1");
830 assert!(!tokens[1].flags.has(Flags::BLOCK_COMMENT));
831 assert!(tokens[1].flags.has(Flags::NEWLINE));
832 }
833
834 #[test]
835 fn the_first_token_is_preceded_by_nothing() {
836 let tokens = all("\n/*x*/ SELECT");
837 assert_eq!(tokens[0].flags, Flags::default());
838 }
839
840 #[test]
841 fn block_comments_nest() {
842 assert_eq!(texts("SELECT /* a /* b */ c */ 1"), ["SELECT", "1"]);
845 assert_eq!(
846 message("SELECT /* a /* b */ 1"),
847 "unterminated /* comment at or near \"/* a /* b */ 1\""
848 );
849 }
850
851 #[test]
852 fn a_string_keeps_its_quotes_and_its_escapes() {
853 assert_eq!(scan("'it''s'"), [(Kind::String, "'it''s'")]);
854 assert_eq!(scan("''"), [(Kind::String, "''")]);
855 assert_eq!(texts("'a' 'b'"), ["'a'", "'b'"]);
856 }
857
858 #[test]
859 fn only_the_e_prefix_changes_how_a_string_is_read() {
860 assert_eq!(scan(r"E'\''"), [(Kind::String, r"E'\''")]);
864 assert_eq!(message(r"'\''"), "unterminated string literal");
865 for prefix in ["X", "x", "B", "b", "N", "n", "E", "e"] {
866 let query = format!("{prefix}'a'");
867 assert_eq!(tokenize(&query).expect("tokenizes")[0].kind, Kind::String);
868 }
869 assert_eq!(texts("x 'a'"), ["x", "'a'"]);
871 }
872
873 #[test]
874 fn a_dollar_quoted_string_is_one_token_and_its_tag_has_to_match() {
875 assert_eq!(scan("$$abc$$"), [(Kind::String, "$$abc$$")]);
876 assert_eq!(scan("$tag$abc$tag$"), [(Kind::String, "$tag$abc$tag$")]);
877 assert_eq!(scan("$tag$a$other$b$tag$"), [(Kind::String, "$tag$a$other$b$tag$")]);
878 assert_eq!(scan("$$it's fine$$"), [(Kind::String, "$$it's fine$$")]);
879 }
880
881 #[test]
882 fn an_unterminated_dollar_quote_is_a_token_and_not_an_error() {
883 let tokens = all("$$abc");
886 assert_eq!(tokens[0].kind, Kind::String);
887 assert!(tokens[0].flags.has(Flags::UNTERMINATED));
888 assert_eq!(tokens[0].text("$$abc"), "$$abc");
889 }
890
891 #[test]
892 fn a_parameter_is_two_tokens() {
893 assert_eq!(scan("$1"), [(Kind::Operator, "$"), (Kind::Number, "1")]);
896 assert_eq!(scan("$banana"), [(Kind::Operator, "$"), (Kind::Identifier, "banana")]);
897 assert_eq!(scan("?"), [(Kind::Operator, "?")]);
898 assert_eq!(scan("$"), [(Kind::Identifier, "$")]);
900 }
901
902 #[test]
903 fn a_semicolon_is_its_own_kind() {
904 assert_eq!(
905 scan("SELECT 1; SELECT 2"),
906 [
907 (Kind::Keyword, "SELECT"),
908 (Kind::Number, "1"),
909 (Kind::Terminator, ";"),
910 (Kind::Keyword, "SELECT"),
911 (Kind::Number, "2"),
912 ]
913 );
914 assert_eq!(scan(";"), [(Kind::Terminator, ";")]);
915 }
916
917 #[test]
918 fn the_four_errors_are_the_four_upstream_throws() {
919 assert_eq!(message("SELECT /* x"), "unterminated /* comment at or near \"/* x\"");
920 assert_eq!(message("SELECT 'x"), "unterminated string literal");
921 assert_eq!(message("SELECT \"x"), "unterminated quoted identifier");
922 assert_eq!(message("SELECT \"\""), "zero-length delimited identifier");
923 assert_eq!(tokenize("SELECT 'x").expect_err("fails").span().map(|s| s.start), Some(7));
925 }
926
927 #[test]
928 fn every_span_lands_where_the_text_is() {
929 let query = "SELECT a, /*c*/ 'b' || $$d$$ FROM t;";
930 for token in tokenize(query).expect("tokenizes") {
931 assert!(token.end as usize <= query.len());
932 assert!(token.start <= token.end);
933 if token.kind != Kind::EndOfInput {
934 assert!(!token.text(query).is_empty());
935 }
936 }
937 }
938
939 #[test]
940 fn a_real_query_comes_out_the_way_it_reads() {
941 assert_eq!(
942 scan("SELECT count(*) FROM t WHERE x > 5 AND y::VARCHAR = 'a';"),
943 [
944 (Kind::Keyword, "SELECT"),
945 (Kind::Identifier, "count"),
946 (Kind::Operator, "("),
947 (Kind::Operator, "*"),
948 (Kind::Operator, ")"),
949 (Kind::Keyword, "FROM"),
950 (Kind::Identifier, "t"),
951 (Kind::Keyword, "WHERE"),
952 (Kind::Identifier, "x"),
953 (Kind::Operator, ">"),
954 (Kind::Number, "5"),
955 (Kind::Keyword, "AND"),
956 (Kind::Identifier, "y"),
957 (Kind::Operator, "::"),
958 (Kind::Keyword, "VARCHAR"),
959 (Kind::Operator, "="),
960 (Kind::String, "'a'"),
961 (Kind::Terminator, ";"),
962 ]
963 );
964 }
965}