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
588#[must_use]
602pub fn quoted(text: &str) -> String {
603 let mut bytes = text.bytes();
604 let plain = matches!(bytes.next(), Some(byte) if byte.is_ascii_alphabetic() || byte == b'_')
605 && bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_');
606 if plain && classes(lookup(text)) == 0 {
607 return text.to_string();
608 }
609 format!("\"{}\"", text.replace('"', "\"\""))
610}
611
612#[must_use]
632pub fn identifier_parts(text: &str) -> Vec<String> {
633 let mut parts = Vec::new();
634 let mut part = String::new();
635 let mut rest = text.chars().peekable();
636 while let Some(c) = rest.next() {
637 match c {
638 '.' => parts.push(std::mem::take(&mut part)),
639 '"' => {
640 while let Some(inside) = rest.next() {
641 if inside != '"' {
642 part.push(inside);
643 continue;
644 }
645 if rest.peek() == Some(&'"') {
648 rest.next();
649 part.push('"');
650 } else {
651 break;
652 }
653 }
654 }
655 _ => part.push(c),
656 }
657 }
658 parts.push(part);
659 parts
660}
661
662const fn is_space(c: u8) -> bool {
663 matches!(c, b' ' | b'\t' | b'\n' | 0x0b | 0x0c | b'\r')
664}
665
666const fn is_single_byte_operator(c: u8) -> bool {
671 matches!(c, b'(' | b')' | b'{' | b'}' | b'[' | b']' | b',' | b'?' | b'$' | b'-' | b'#')
672}
673
674const fn is_operator_char(c: u8) -> bool {
676 if c == b'_' {
677 return false;
678 }
679 matches!(c, b'!'..=b'/' | b':'..=b'@' | b'['..=b'`' | b'{'..=b'~')
680}
681
682const fn is_operator_char_in_run(c: u8) -> bool {
688 if is_single_byte_operator(c) || is_control_flow(c) {
689 return false;
690 }
691 is_operator_char(c)
692}
693
694const fn is_control_flow(c: u8) -> bool {
695 matches!(c, b'\'' | b'-' | b';' | b'"' | b'.')
696}
697
698const fn is_word_char(c: u8) -> bool {
704 if is_single_byte_operator(c) || is_operator_char(c) || is_space(c) || is_control_flow(c) {
705 return false;
706 }
707 true
708}
709
710const fn is_initial_number(c: u8) -> bool {
715 c.is_ascii_digit() || c == b'.'
716}
717
718const fn is_scientific(c: u8) -> bool {
719 c == b'e' || c == b'E'
720}
721
722const fn is_string_prefix(c: u8) -> bool {
723 matches!(c, b'N' | b'n' | b'X' | b'x' | b'E' | b'e' | b'B' | b'b')
724}
725
726const fn is_dollar_tag_char(c: u8) -> bool {
729 c.is_ascii_alphanumeric() || c == b'_' || c >= 0x80
730}
731
732fn special_operator(bytes: &[u8], at: usize) -> Option<usize> {
737 if bytes[at..].starts_with(b"->>") {
738 return Some(3);
739 }
740 for candidate in [b"::".as_slice(), b":=", b"->", b"**", b"//"] {
741 if bytes[at..].starts_with(candidate) {
742 return Some(2);
743 }
744 }
745 None
746}
747
748#[cfg(test)]
749mod tests {
750 use super::{classes, identifier_parts, lookup, quoted, tokenize};
751 use crate::generated::keywords::{RESERVED, UNRESERVED};
752 use crate::token::{Flags, Kind, NOT_A_KEYWORD, Token};
753
754 #[test]
755 fn a_deparsed_identifier_is_quoted_on_a_keyword_or_a_word_that_will_not_read_back() {
756 assert_eq!(quoted("name"), "\"name\"");
758 assert_eq!(quoted("alias"), "alias");
762 assert_eq!(quoted("x"), "x");
763 assert_eq!(quoted("_x9"), "_x9");
764 assert_eq!(quoted("my col"), "\"my col\"");
765 assert_eq!(quoted("9x"), "\"9x\"");
766 assert_eq!(quoted("e\u{301}"), "\"e\u{301}\"");
767 assert_eq!(quoted("UserID"), "UserID");
770 assert_eq!(quoted("a\"b"), "\"a\"\"b\"");
772 }
773
774 fn scan(query: &str) -> Vec<(Kind, &str)> {
776 let tokens = tokenize(query).expect("tokenizes");
777 assert_eq!(tokens.last().map(|t| t.kind), Some(Kind::EndOfInput));
778 assert_eq!(tokens.iter().filter(|t| t.kind == Kind::EndOfInput).count(), 1);
779 tokens[..tokens.len() - 1].iter().map(|t| (t.kind, t.text(query))).collect()
780 }
781
782 fn texts(query: &str) -> Vec<&str> {
783 scan(query).into_iter().map(|(_, text)| text).collect()
784 }
785
786 fn all(query: &str) -> Vec<Token> {
787 tokenize(query).expect("tokenizes")
788 }
789
790 fn message(query: &str) -> String {
791 tokenize(query).expect_err("fails").message().to_string()
792 }
793
794 #[test]
795 fn the_empty_query_is_one_sentinel() {
796 let tokens = tokenize("").expect("tokenizes");
797 assert_eq!(tokens.len(), 1);
798 assert_eq!(tokens[0].kind, Kind::EndOfInput);
799 assert_eq!(tokens[0].span(), rudb_common::Span::new(0, 0));
800 assert!(scan(" \t\n ").is_empty());
801 }
802
803 #[test]
804 fn a_word_in_a_class_is_a_keyword_and_one_in_none_is_not() {
805 assert_eq!(scan("SELECT"), [(Kind::Keyword, "SELECT")]);
806 assert_eq!(scan("banana"), [(Kind::Identifier, "banana")]);
807 }
808
809 #[test]
810 fn case_is_matched_but_not_folded() {
811 for spelling in ["select", "SELECT", "SeLeCt"] {
815 let tokens = all(spelling);
816 assert_eq!(tokens[0].kind, Kind::Keyword);
817 assert_eq!(tokens[0].text(spelling), spelling);
818 assert_eq!(tokens[0].keyword, lookup("select"));
819 }
820 assert_eq!(scan(r#""Foo""#), [(Kind::QuotedIdentifier, r#""Foo""#)]);
821 }
822
823 #[test]
824 fn a_soft_word_keeps_its_index_and_stays_an_identifier() {
825 let tokens = all("ascending");
829 assert_eq!(tokens[0].kind, Kind::Identifier);
830 assert_ne!(tokens[0].keyword, NOT_A_KEYWORD);
831 assert_eq!(classes(tokens[0].keyword), 0);
832
833 let tokens = all("banana");
834 assert_eq!(tokens[0].keyword, NOT_A_KEYWORD);
835 assert_eq!(classes(tokens[0].keyword), 0);
836 }
837
838 #[test]
839 fn the_classes_come_back_off_the_index() {
840 assert_eq!(classes(lookup("select")) & RESERVED, RESERVED);
841 assert_eq!(classes(lookup("abort")) & UNRESERVED, UNRESERVED);
842 assert_eq!(lookup("supercalifragilistic"), NOT_A_KEYWORD);
843 assert_eq!(lookup(""), NOT_A_KEYWORD);
844 }
845
846 #[test]
847 fn a_quoted_identifier_is_never_a_keyword() {
848 let tokens = all(r#""select""#);
849 assert_eq!(tokens[0].kind, Kind::QuotedIdentifier);
850 assert_eq!(tokens[0].keyword, NOT_A_KEYWORD);
851 assert_eq!(scan(r#""a""b""#), [(Kind::QuotedIdentifier, r#""a""b""#)]);
852 }
853
854 #[test]
855 fn a_dollar_is_an_identifier_character_after_the_first_byte() {
856 assert_eq!(scan("a$b"), [(Kind::Identifier, "a$b")]);
857 }
858
859 #[test]
860 fn any_byte_above_ascii_is_an_identifier_character() {
861 assert_eq!(scan("SELECT café"), [(Kind::Keyword, "SELECT"), (Kind::Identifier, "café")]);
865 }
866
867 #[test]
868 fn a_number_swallows_more_than_a_number() {
869 assert_eq!(scan("1.2.3"), [(Kind::Number, "1.2.3")]);
872 assert_eq!(scan("1_000"), [(Kind::Number, "1_000")]);
873 assert_eq!(scan("1e5"), [(Kind::Number, "1e5")]);
874 assert_eq!(scan(".1e5"), [(Kind::Number, ".1e5")]);
875 assert_eq!(scan("1e-5"), [(Kind::Number, "1e-5")]);
876 assert_eq!(scan("1.e5"), [(Kind::Number, "1.e5")]);
877 }
878
879 #[test]
880 fn a_trailing_e_stays_on_the_number() {
881 assert_eq!(scan("SELECT 1e"), [(Kind::Keyword, "SELECT"), (Kind::Number, "1e")]);
884 assert_eq!(scan("SELECT 1e+"), [(Kind::Keyword, "SELECT"), (Kind::Number, "1e+")]);
885 }
886
887 #[test]
888 fn what_the_number_cannot_use_it_gives_back() {
889 assert_eq!(
892 scan("SELECT 1e+ 1"),
893 [
894 (Kind::Keyword, "SELECT"),
895 (Kind::Number, "1"),
896 (Kind::Identifier, "e"),
897 (Kind::Operator, "+"),
898 (Kind::Number, "1"),
899 ]
900 );
901 assert_eq!(scan("1_"), [(Kind::Number, "1"), (Kind::Identifier, "_")]);
902 assert_eq!(scan("1__0"), [(Kind::Number, "1"), (Kind::Identifier, "__0")]);
903 assert_eq!(scan("0x1F"), [(Kind::Number, "0"), (Kind::Identifier, "x1F")]);
906 assert_eq!(scan(".e100"), [(Kind::Number, "."), (Kind::Identifier, "e100")]);
907 }
908
909 #[test]
910 fn a_minus_never_joins_an_operator_run() {
911 assert_eq!(texts("1-1"), ["1", "-", "1"]);
914 assert_eq!(texts("SELECT 1 =- 1"), ["SELECT", "1", "=", "-", "1"]);
915 assert_eq!(texts("(a,b)"), ["(", "a", ",", "b", ")"]);
916 }
917
918 #[test]
919 fn the_postgres_plus_rule_decides_where_a_run_ends() {
920 assert_eq!(texts("SELECT 1 =+ 1"), ["SELECT", "1", "=", "+", "1"]);
923 assert_eq!(texts("SELECT 1 !=+ 1"), ["SELECT", "1", "!=+", "1"]);
924 assert_eq!(texts("SELECT 1 =++ 1"), ["SELECT", "1", "=", "+", "+", "1"]);
925 assert_eq!(texts("SELECT 1 ++ 1"), ["SELECT", "1", "+", "+", "1"]);
926 }
927
928 #[test]
929 fn the_special_operators_are_checked_before_the_run() {
930 assert_eq!(texts("a->>'b'"), ["a", "->>", "'b'"]);
931 assert_eq!(texts("a->'b'"), ["a", "->", "'b'"]);
932 assert_eq!(texts("a::b"), ["a", "::", "b"]);
933 assert_eq!(texts("a//b"), ["a", "//", "b"]);
934 assert_eq!(texts("2**3"), ["2", "**", "3"]);
935 assert_eq!(texts("a::=b"), ["a", "::=", "b"]);
938 }
939
940 #[test]
941 fn a_block_comment_can_end_an_operator_run() {
942 assert_eq!(texts("1+/*c*/2"), ["1", "+", "2"]);
943 }
944
945 #[test]
946 fn a_comment_is_not_a_token_but_a_block_one_leaves_a_mark() {
947 assert_eq!(texts("SELECT --x\n1"), ["SELECT", "1"]);
948 assert_eq!(texts("SELECT /*x*/ 1"), ["SELECT", "1"]);
949 assert_eq!(texts("SELECT --x"), ["SELECT"]);
950
951 let tokens = all("SELECT /*x*/ 1");
952 assert!(tokens[1].flags.has(Flags::BLOCK_COMMENT));
953 assert!(!tokens[1].flags.has(Flags::NEWLINE));
954
955 let tokens = all("SELECT --x\n1");
957 assert!(!tokens[1].flags.has(Flags::BLOCK_COMMENT));
958 assert!(tokens[1].flags.has(Flags::NEWLINE));
959 }
960
961 #[test]
962 fn a_hint_is_a_comment_that_can_be_read_back() {
963 assert_eq!(
964 super::hints("SELECT /*+ hash.table(unchained) */ 1").unwrap(),
965 [" hash.table(unchained) "]
966 );
967 assert_eq!(texts("SELECT /*+ hash.table(unchained) */ 1"), ["SELECT", "1"]);
969
970 assert_eq!(super::hints("SELECT /*+ a(b) */ 1 /*+ c(d) */").unwrap(), [" a(b) ", " c(d) "]);
972
973 assert!(super::hints("SELECT /* hash.table(unchained) */ 1").unwrap().is_empty());
976 assert!(super::hints("SELECT '/*+ hash.table(unchained) */'").unwrap().is_empty());
977 assert!(super::hints("SELECT 1").unwrap().is_empty());
978 }
979
980 #[test]
981 fn the_first_token_is_preceded_by_nothing() {
982 let tokens = all("\n/*x*/ SELECT");
983 assert_eq!(tokens[0].flags, Flags::default());
984 }
985
986 #[test]
987 fn block_comments_nest() {
988 assert_eq!(texts("SELECT /* a /* b */ c */ 1"), ["SELECT", "1"]);
991 assert_eq!(
992 message("SELECT /* a /* b */ 1"),
993 "unterminated /* comment at or near \"/* a /* b */ 1\""
994 );
995 }
996
997 #[test]
998 fn a_string_keeps_its_quotes_and_its_escapes() {
999 assert_eq!(scan("'it''s'"), [(Kind::String, "'it''s'")]);
1000 assert_eq!(scan("''"), [(Kind::String, "''")]);
1001 assert_eq!(texts("'a' 'b'"), ["'a'", "'b'"]);
1002 }
1003
1004 #[test]
1005 fn only_the_e_prefix_changes_how_a_string_is_read() {
1006 assert_eq!(scan(r"E'\''"), [(Kind::String, r"E'\''")]);
1010 assert_eq!(message(r"'\''"), "unterminated string literal");
1011 for prefix in ["X", "x", "B", "b", "N", "n", "E", "e"] {
1012 let query = format!("{prefix}'a'");
1013 assert_eq!(tokenize(&query).expect("tokenizes")[0].kind, Kind::String);
1014 }
1015 assert_eq!(texts("x 'a'"), ["x", "'a'"]);
1017 }
1018
1019 #[test]
1020 fn a_dollar_quoted_string_is_one_token_and_its_tag_has_to_match() {
1021 assert_eq!(scan("$$abc$$"), [(Kind::String, "$$abc$$")]);
1022 assert_eq!(scan("$tag$abc$tag$"), [(Kind::String, "$tag$abc$tag$")]);
1023 assert_eq!(scan("$tag$a$other$b$tag$"), [(Kind::String, "$tag$a$other$b$tag$")]);
1024 assert_eq!(scan("$$it's fine$$"), [(Kind::String, "$$it's fine$$")]);
1025 }
1026
1027 #[test]
1028 fn an_unterminated_dollar_quote_is_a_token_and_not_an_error() {
1029 let tokens = all("$$abc");
1032 assert_eq!(tokens[0].kind, Kind::String);
1033 assert!(tokens[0].flags.has(Flags::UNTERMINATED));
1034 assert_eq!(tokens[0].text("$$abc"), "$$abc");
1035 }
1036
1037 #[test]
1038 fn a_parameter_is_two_tokens() {
1039 assert_eq!(scan("$1"), [(Kind::Operator, "$"), (Kind::Number, "1")]);
1042 assert_eq!(scan("$banana"), [(Kind::Operator, "$"), (Kind::Identifier, "banana")]);
1043 assert_eq!(scan("?"), [(Kind::Operator, "?")]);
1044 assert_eq!(scan("$"), [(Kind::Identifier, "$")]);
1046 }
1047
1048 #[test]
1049 fn a_semicolon_is_its_own_kind() {
1050 assert_eq!(
1051 scan("SELECT 1; SELECT 2"),
1052 [
1053 (Kind::Keyword, "SELECT"),
1054 (Kind::Number, "1"),
1055 (Kind::Terminator, ";"),
1056 (Kind::Keyword, "SELECT"),
1057 (Kind::Number, "2"),
1058 ]
1059 );
1060 assert_eq!(scan(";"), [(Kind::Terminator, ";")]);
1061 }
1062
1063 #[test]
1064 fn the_four_errors_are_the_four_upstream_throws() {
1065 assert_eq!(message("SELECT /* x"), "unterminated /* comment at or near \"/* x\"");
1066 assert_eq!(message("SELECT 'x"), "unterminated string literal");
1067 assert_eq!(message("SELECT \"x"), "unterminated quoted identifier");
1068 assert_eq!(message("SELECT \"\""), "zero-length delimited identifier");
1069 assert_eq!(tokenize("SELECT 'x").expect_err("fails").span().map(|s| s.start), Some(7));
1071 }
1072
1073 #[test]
1074 fn every_span_lands_where_the_text_is() {
1075 let query = "SELECT a, /*c*/ 'b' || $$d$$ FROM t;";
1076 for token in tokenize(query).expect("tokenizes") {
1077 assert!(token.end as usize <= query.len());
1078 assert!(token.start <= token.end);
1079 if token.kind != Kind::EndOfInput {
1080 assert!(!token.text(query).is_empty());
1081 }
1082 }
1083 }
1084
1085 #[test]
1086 fn a_real_query_comes_out_the_way_it_reads() {
1087 assert_eq!(
1088 scan("SELECT count(*) FROM t WHERE x > 5 AND y::VARCHAR = 'a';"),
1089 [
1090 (Kind::Keyword, "SELECT"),
1091 (Kind::Identifier, "count"),
1092 (Kind::Operator, "("),
1093 (Kind::Operator, "*"),
1094 (Kind::Operator, ")"),
1095 (Kind::Keyword, "FROM"),
1096 (Kind::Identifier, "t"),
1097 (Kind::Keyword, "WHERE"),
1098 (Kind::Identifier, "x"),
1099 (Kind::Operator, ">"),
1100 (Kind::Number, "5"),
1101 (Kind::Keyword, "AND"),
1102 (Kind::Identifier, "y"),
1103 (Kind::Operator, "::"),
1104 (Kind::Keyword, "VARCHAR"),
1105 (Kind::Operator, "="),
1106 (Kind::String, "'a'"),
1107 (Kind::Terminator, ";"),
1108 ]
1109 );
1110 }
1111
1112 #[test]
1114 fn a_dotted_name_splits_where_the_tokenizer_would_split_it() {
1115 assert_eq!(identifier_parts("t"), ["t"]);
1116 assert_eq!(identifier_parts("main.t"), ["main", "t"]);
1117 assert_eq!(identifier_parts("memory.main.t"), ["memory", "main", "t"]);
1118 assert_eq!(identifier_parts("Memory.Main.MyTable"), ["Memory", "Main", "MyTable"]);
1120 }
1121
1122 #[test]
1125 fn a_quoted_part_holds_the_dots_that_are_inside_it() {
1126 assert_eq!(identifier_parts("\"a.b\""), ["a.b"]);
1127 assert_eq!(identifier_parts("\"a.b\".c"), ["a.b", "c"]);
1128 assert_eq!(identifier_parts("\"a\"\"b\""), ["a\"b"]);
1129 assert_eq!(identifier_parts("main.\"My Table\""), ["main", "My Table"]);
1130 assert_eq!(identifier_parts("\"a.b"), ["a.b"]);
1132 }
1133
1134 #[test]
1136 fn an_empty_name_is_one_empty_part_rather_than_nothing() {
1137 assert_eq!(identifier_parts(""), [""]);
1138 assert_eq!(identifier_parts("."), ["", ""]);
1139 assert_eq!(identifier_parts("a."), ["a", ""]);
1140 assert_eq!(identifier_parts("memory..t"), ["memory", "", "t"]);
1141 }
1142}