1mod cursor;
2mod token;
3use cursor::{Cursor, EOF_CHAR};
4pub use token::{Base, LiteralKind, Token, TokenKind};
5
6const fn is_ident_start(c: char) -> bool {
9 matches!(c, 'a'..='z' | 'A'..='Z' | '_' | '\u{80}'..)
10}
11
12const fn is_ident_cont(c: char) -> bool {
14 matches!(c, 'a'..='z' | 'A'..='Z' | '_' | '0'..='9' | '$' | '\u{80}'..)
15}
16
17pub const BOM: &str = "\u{feff}";
18
19const fn is_whitespace(c: char) -> bool {
23 matches!(
24 c,
25 ' ' | '\t' | '\n' | '\r' | '\u{000B}' | '\u{000C}' )
32}
33
34impl Cursor<'_> {
35 pub(crate) fn advance_token(&mut self) -> Token {
37 let Some(first_char) = self.bump() else {
38 return Token::new(TokenKind::Eof, 0);
39 };
40 let token_kind = match first_char {
41 '/' => match self.first() {
43 '*' => self.block_comment(),
44 _ => TokenKind::Slash,
45 },
46 '-' => match self.first() {
47 '-' => self.line_comment(),
48 _ => TokenKind::Minus,
49 },
50
51 c if is_whitespace(c) => self.whitespace(),
53
54 'u' | 'U' => {
56 if self.first() == '&' && matches!(self.second(), '\'' | '"') {
57 self.bump();
58 self.prefixed_string(
59 |terminated| LiteralKind::UnicodeEscStr { terminated },
60 true,
61 false,
62 )
63 } else {
64 self.ident()
65 }
66 }
67 'e' | 'E' => {
69 self.prefixed_string(|terminated| LiteralKind::EscStr { terminated }, false, true)
70 }
71
72 'b' | 'B' => self.prefixed_string(
74 |terminated| LiteralKind::BitStr { terminated },
75 false,
76 false,
77 ),
78
79 'x' | 'X' => self.prefixed_string(
81 |terminated| LiteralKind::ByteStr { terminated },
82 false,
83 false,
84 ),
85
86 'n' | 'N' => match self.first() {
88 '\'' => {
89 self.bump();
90 let terminated = self.single_quoted_string(false);
91 TokenKind::Literal {
92 kind: LiteralKind::NationalStr { terminated },
93 }
94 }
95 _ => self.ident(),
96 },
97
98 c if is_ident_start(c) => self.ident(),
101
102 c @ '0'..='9' => {
105 let literal_kind = self.number(c);
106 TokenKind::Literal { kind: literal_kind }
107 }
108 '.' => match self.first() {
109 '0'..='9' => {
110 let literal_kind = self.number('.');
111 TokenKind::Literal { kind: literal_kind }
112 }
113 _ => TokenKind::Dot,
114 },
115 ';' => TokenKind::Semi,
117 ',' => TokenKind::Comma,
118 '(' => TokenKind::OpenParen,
119 ')' => TokenKind::CloseParen,
120 '[' => TokenKind::OpenBracket,
121 ']' => TokenKind::CloseBracket,
122 '{' => TokenKind::OpenCurly,
123 '}' => TokenKind::CloseCurly,
124 '@' => TokenKind::At,
125 '#' => TokenKind::Pound,
126 '~' => TokenKind::Tilde,
127 '?' => TokenKind::Question,
128 ':' => TokenKind::Colon,
129 '$' => {
130 if self.is_dollar_quote_start() {
131 self.dollar_quoted_string()
132 } else {
133 while self.first().is_ascii_digit() {
135 self.bump();
136 }
137 let trailing_junk_start = self.pos_within_token();
138 self.eat_identifier();
139 TokenKind::PositionalParam {
140 trailing_junk_start,
141 }
142 }
143 }
144 '`' => TokenKind::Backtick,
145 '=' => TokenKind::Eq,
146 '!' => TokenKind::Bang,
147 '<' => TokenKind::Lt,
148 '>' => TokenKind::Gt,
149 '&' => TokenKind::And,
150 '|' => TokenKind::Or,
151 '+' => TokenKind::Plus,
152 '*' => TokenKind::Star,
153 '^' => TokenKind::Caret,
154 '%' => TokenKind::Percent,
155
156 '\'' => {
158 let terminated = self.single_quoted_string(false);
159 let kind = LiteralKind::Str { terminated };
160 TokenKind::Literal { kind }
161 }
162
163 '"' => {
165 let terminated = self.double_quoted_string();
166 TokenKind::QuotedIdent {
167 terminated,
168 uescape: false,
169 }
170 }
171 _ => TokenKind::Unknown,
172 };
173 let res = Token::new(token_kind, self.pos_within_token());
174 self.reset_pos_within_token();
175 res
176 }
177 pub(crate) fn ident(&mut self) -> TokenKind {
178 self.eat_while(is_ident_cont);
179 TokenKind::Ident
180 }
181
182 pub(crate) fn whitespace(&mut self) -> TokenKind {
183 self.eat_while(is_whitespace);
184 TokenKind::Whitespace
185 }
186
187 pub(crate) fn line_comment(&mut self) -> TokenKind {
190 self.bump();
191
192 self.eat_while(|c| c != '\n' && c != '\r');
193 TokenKind::LineComment
194 }
195
196 pub(crate) fn block_comment(&mut self) -> TokenKind {
198 self.bump();
199
200 let mut depth = 1usize;
201 while let Some(c) = self.bump() {
202 match c {
203 '/' if self.first() == '*' => {
204 self.bump();
205 depth += 1;
206 }
207 '*' if self.first() == '/' => {
208 self.bump();
209 depth -= 1;
210 if depth == 0 {
211 break;
215 }
216 }
217 _ => (),
218 }
219 }
220
221 TokenKind::BlockComment {
222 terminated: depth == 0,
223 }
224 }
225
226 fn prefixed_string(
227 &mut self,
228 mk_kind: fn(bool) -> LiteralKind,
229 allows_double: bool,
230 backslash_escapes: bool,
231 ) -> TokenKind {
232 match self.first() {
233 '\'' => {
234 self.bump();
235 let terminated = self.single_quoted_string(backslash_escapes);
236 let kind = mk_kind(terminated);
237 TokenKind::Literal { kind }
238 }
239 '"' if allows_double => {
240 self.bump();
241 let terminated = self.double_quoted_string();
242 TokenKind::QuotedIdent {
243 terminated,
244 uescape: true,
245 }
246 }
247 _ => self.ident(),
248 }
249 }
250
251 fn number(&mut self, first_digit: char) -> LiteralKind {
252 let mut base = Base::Decimal;
253 if first_digit == '.' {
254 return self.eat_fractional();
255 }
256 if first_digit == '0' {
257 match self.first() {
259 'b' | 'B' => {
261 base = Base::Binary;
262 self.bump();
263 let has_digits = self.eat_decimal_digits();
264 return self.finish_base_prefixed_int(base, has_digits);
265 }
266 'o' | 'O' => {
268 base = Base::Octal;
269 self.bump();
270 let has_digits = self.eat_decimal_digits();
271 return self.finish_base_prefixed_int(base, has_digits);
272 }
273 'x' | 'X' => {
275 base = Base::Hexadecimal;
276 self.bump();
277 let has_digits = self.eat_hexadecimal_digits();
278 return self.finish_base_prefixed_int(base, has_digits);
279 }
280 '0'..='9' | '_' => {
282 self.eat_decimal_digits();
283 }
284
285 '.' | 'e' | 'E' => {}
287
288 _ => {
290 let trailing_junk_start = self.pos_within_token();
291 self.eat_identifier();
292 return LiteralKind::Int {
293 base,
294 empty_int: false,
295 trailing_junk_start,
296 };
297 }
298 }
299 } else {
300 self.eat_decimal_digits();
302 };
303
304 match self.first() {
305 '.' => {
306 self.bump();
307 self.eat_fractional()
308 }
309 'e' | 'E' => {
310 let exponent_start = self.pos_within_token();
311 self.bump();
312 let empty_exponent_start = (!self.eat_numeric_exponent()).then_some(exponent_start);
313 let trailing_junk_start = self.pos_within_token();
314 self.eat_identifier();
315 LiteralKind::Numeric {
316 empty_exponent_start,
317 trailing_junk_start,
318 }
319 }
320 _ => {
321 let trailing_junk_start = self.pos_within_token();
322 self.eat_identifier();
323 LiteralKind::Int {
324 base,
325 empty_int: false,
326 trailing_junk_start,
327 }
328 }
329 }
330 }
331
332 fn single_quoted_string(&mut self, backslash_escapes: bool) -> bool {
333 loop {
335 match self.first() {
336 '\\' if backslash_escapes => {
337 self.bump();
339 self.bump();
341 }
342 '\'' => {
344 self.bump();
345
346 match self.first() {
347 '\'' => {
349 self.bump();
350 }
351 _ => return true,
353 }
354 }
355 EOF_CHAR if self.is_eof() => break,
357 _ => {
359 self.bump();
360 }
361 }
362 }
363 false
365 }
366
367 fn double_quoted_string(&mut self) -> bool {
370 while let Some(c) = self.bump() {
371 match c {
372 '"' if self.first() == '"' => {
373 self.bump();
375 }
376 '"' => {
377 return true;
378 }
379 _ => (),
380 }
381 }
382 false
384 }
385
386 fn is_dollar_quote_start(&self) -> bool {
388 let mut chars = self.chars();
389 match chars.next() {
390 Some('$') => true,
392 Some(c) if is_ident_start(c) => {
394 for c in chars {
395 if c == '$' {
396 return true;
397 }
398 if !is_ident_cont(c) {
399 return false;
400 }
401 }
402 false
403 }
404 _ => false,
405 }
406 }
407
408 fn dollar_quoted_string(&mut self) -> TokenKind {
410 let mut start = vec![];
413 while let Some(c) = self.bump() {
414 match c {
415 '$' => {
416 break;
417 }
418 _ => {
419 start.push(c);
420 }
421 }
422 }
423
424 if start.is_empty() {
426 loop {
427 self.eat_while(|c| c != '$');
428 if self.is_eof() {
429 return TokenKind::Literal {
430 kind: LiteralKind::DollarQuotedString { terminated: false },
431 };
432 }
433 self.bump();
435 if self.first() == '$' {
436 self.bump();
437 return TokenKind::Literal {
438 kind: LiteralKind::DollarQuotedString { terminated: true },
439 };
440 }
441 }
442 } else {
443 loop {
444 self.eat_while(|c| c != '$');
445 if self.is_eof() {
446 return TokenKind::Literal {
447 kind: LiteralKind::DollarQuotedString { terminated: false },
448 };
449 }
450
451 self.bump();
453
454 let mut matches_tag = true;
455 for start_char in &start {
456 if self.first() == *start_char {
457 self.bump();
458 } else {
459 matches_tag = false;
460 break;
461 }
462 }
463
464 if matches_tag && self.first() == '$' {
465 self.bump();
466 return TokenKind::Literal {
467 kind: LiteralKind::DollarQuotedString { terminated: true },
468 };
469 }
470 }
471 }
472 }
473
474 fn eat_decimal_digits(&mut self) -> bool {
475 let mut has_digits = false;
476 loop {
477 match self.first() {
478 '_' if self.second().is_ascii_digit() => {
479 self.bump();
480 }
481 '0'..='9' => {
482 has_digits = true;
483 self.bump();
484 }
485 _ => break,
486 }
487 }
488 has_digits
489 }
490
491 fn finish_base_prefixed_int(&mut self, base: Base, has_digits: bool) -> LiteralKind {
492 let trailing_junk_start = self.pos_within_token();
493 self.eat_identifier();
494 let has_trailing_junk = self.pos_within_token() > trailing_junk_start;
495 LiteralKind::Int {
496 base,
497 empty_int: !has_digits && !has_trailing_junk,
498 trailing_junk_start,
499 }
500 }
501
502 fn eat_hexadecimal_digits(&mut self) -> bool {
503 let mut has_digits = false;
504 loop {
505 match self.first() {
506 '_' if self.second().is_ascii_hexdigit() => {
507 self.bump();
508 }
509 '0'..='9' | 'a'..='f' | 'A'..='F' => {
510 has_digits = true;
511 self.bump();
512 }
513 _ => break,
514 }
515 }
516 has_digits
517 }
518
519 fn eat_numeric_exponent(&mut self) -> bool {
522 if self.first() == '-' || self.first() == '+' {
523 if !self.second().is_ascii_digit() {
524 return false;
525 }
526 self.bump();
527 } else if !self.first().is_ascii_digit() {
528 return false;
529 }
530 self.eat_decimal_digits()
531 }
532
533 fn eat_identifier(&mut self) {
534 if is_ident_start(self.first()) {
535 self.eat_while(is_ident_cont);
536 }
537 }
538
539 pub(crate) fn eat_fractional(&mut self) -> crate::LiteralKind {
540 let mut empty_exponent_start = None;
541 if self.first().is_ascii_digit() {
542 self.eat_decimal_digits();
543 }
544 match self.first() {
545 'e' | 'E' => {
546 let exponent_start = self.pos_within_token();
547 self.bump();
548 if !self.eat_numeric_exponent() {
549 empty_exponent_start = Some(exponent_start);
550 }
551 }
552 _ => (),
553 }
554 let trailing_junk_start = self.pos_within_token();
555 self.eat_identifier();
556 LiteralKind::Numeric {
557 empty_exponent_start,
558 trailing_junk_start,
559 }
560 }
561}
562
563pub fn tokenize(input: &str) -> impl Iterator<Item = Token> + '_ {
565 let (bom, input) = match input.strip_prefix(BOM) {
566 Some(input) => (
567 Some(Token::new(TokenKind::Whitespace, BOM.len() as u32)),
568 input,
569 ),
570 None => (None, input),
571 };
572 let mut cursor = Cursor::new(input);
573 bom.into_iter().chain(std::iter::from_fn(move || {
574 let token = cursor.advance_token();
575 if token.kind != TokenKind::Eof {
576 Some(token)
577 } else {
578 None
579 }
580 }))
581}
582
583#[cfg(test)]
584mod tests {
585 use std::fmt;
586
587 use super::*;
588 use insta::assert_debug_snapshot;
589
590 struct TokenDebug<'a> {
591 content: &'a str,
592 token: Token,
593 }
594 impl fmt::Debug for TokenDebug<'_> {
595 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
596 write!(f, "{:?} @ {:?}", self.content, self.token.kind)
597 }
598 }
599
600 impl<'a> TokenDebug<'a> {
601 fn new(token: Token, input: &'a str, start: u32) -> TokenDebug<'a> {
602 TokenDebug {
603 token,
604 content: &input[start as usize..(start + token.len) as usize],
605 }
606 }
607 }
608
609 fn lex(input: &str) -> Vec<TokenDebug<'_>> {
610 let mut tokens = vec![];
611 let mut start = 0;
612
613 for token in tokenize(input) {
614 let length = token.len;
615 tokens.push(TokenDebug::new(token, input, start));
616 start += length;
617 }
618 tokens
619 }
620 #[test]
621 fn lex_statement() {
622 let result = lex("select 1;");
623 assert_debug_snapshot!(result);
624 }
625
626 #[test]
627 fn block_comment() {
628 let result = lex(r#"
629/*
630 * foo
631 * bar
632*/"#);
633 assert_debug_snapshot!(result);
634 }
635
636 #[test]
637 fn block_comment_unterminated() {
638 let result = lex(r#"
639/*
640 * foo
641 * bar
642 /*
643*/"#);
644 assert_debug_snapshot!(result);
645 }
646
647 #[test]
648 fn line_comment() {
649 let result = lex(r#"
650-- foooooooooooo bar buzz
651"#);
652 assert_debug_snapshot!(result);
653 }
654
655 #[test]
656 fn line_comment_cr_newline() {
657 assert_debug_snapshot!(lex("select 1; -- comment\rselect 2;"), @r#"
658 [
659 "select" @ Ident,
660 " " @ Whitespace,
661 "1" @ Literal { kind: Int { base: Decimal, empty_int: false, trailing_junk_start: 1 } },
662 ";" @ Semi,
663 " " @ Whitespace,
664 "-- comment" @ LineComment,
665 "\r" @ Whitespace,
666 "select" @ Ident,
667 " " @ Whitespace,
668 "2" @ Literal { kind: Int { base: Decimal, empty_int: false, trailing_junk_start: 1 } },
669 ";" @ Semi,
670 ]
671 "#);
672 }
673
674 #[test]
675 fn line_comment_whitespace() {
676 assert_debug_snapshot!(lex(r#"
677select 'Hello' -- This is a comment
678' World';"#))
679 }
680
681 #[test]
682 fn dollar_quoting() {
683 assert_debug_snapshot!(lex(r#"
684$$Dianne's horse$$
685$SomeTag$Dianne's horse$SomeTag$
686
687-- with dollar inside and matching tags
688$foo$hello$world$bar$
689"#))
690 }
691
692 #[test]
693 fn dollar_strings_part2() {
694 assert_debug_snapshot!(lex(r#"
695DO $doblock$
696end
697$doblock$;"#))
698 }
699
700 #[test]
701 fn dollar_quote_mismatch_tags_simple() {
702 assert_debug_snapshot!(lex(r#"
703-- dollar quoting with mismatched tags
704$foo$hello world$bar$
705"#));
706 }
707
708 #[test]
709 fn dollar_quote_mismatch_tags_complex() {
710 assert_debug_snapshot!(lex(r#"
711-- with dollar inside but mismatched tags
712$foo$hello$world$bar$
713"#));
714 }
715
716 #[test]
717 fn numeric() {
718 assert_debug_snapshot!(lex(r#"
71942
7203.5
7214.
722.001
723.123e10
7245e2
7251.925e-3
7261e-10
7271e+10
7281e10
7294664.E+5
730"#))
731 }
732
733 #[test]
734 fn numeric_non_decimal() {
735 assert_debug_snapshot!(lex(r#"
7360b100101
7370B10011001
7380o273
7390O755
7400x42f
7410XFFFF
742"#))
743 }
744
745 #[test]
746 fn numeric_base_prefix_does_not_swallow_dollar_tokens() {
747 assert_debug_snapshot!(lex("123$abc 0b101$2 0o12$abc 0x12$abc 0xFF$1 0x1$$foo$$ 123$$foo$$"), @r#"
748 [
749 "123" @ Literal { kind: Int { base: Decimal, empty_int: false, trailing_junk_start: 3 } },
750 "$abc" @ PositionalParam { trailing_junk_start: 1 },
751 " " @ Whitespace,
752 "0b101" @ Literal { kind: Int { base: Binary, empty_int: false, trailing_junk_start: 5 } },
753 "$2" @ PositionalParam { trailing_junk_start: 2 },
754 " " @ Whitespace,
755 "0o12" @ Literal { kind: Int { base: Octal, empty_int: false, trailing_junk_start: 4 } },
756 "$abc" @ PositionalParam { trailing_junk_start: 1 },
757 " " @ Whitespace,
758 "0x12" @ Literal { kind: Int { base: Hexadecimal, empty_int: false, trailing_junk_start: 4 } },
759 "$abc" @ PositionalParam { trailing_junk_start: 1 },
760 " " @ Whitespace,
761 "0xFF" @ Literal { kind: Int { base: Hexadecimal, empty_int: false, trailing_junk_start: 4 } },
762 "$1" @ PositionalParam { trailing_junk_start: 2 },
763 " " @ Whitespace,
764 "0x1" @ Literal { kind: Int { base: Hexadecimal, empty_int: false, trailing_junk_start: 3 } },
765 "$$foo$$" @ Literal { kind: DollarQuotedString { terminated: true } },
766 " " @ Whitespace,
767 "123" @ Literal { kind: Int { base: Decimal, empty_int: false, trailing_junk_start: 3 } },
768 "$$foo$$" @ Literal { kind: DollarQuotedString { terminated: true } },
769 ]
770 "#);
771 }
772
773 #[test]
774 fn numeric_with_seperators() {
775 assert_debug_snapshot!(lex(r#"
7761_500_000_000
7770b10001000_00000000
7780o_1_755
7790xFFFF_FFFF
7801.618_034
781"#))
782 }
783
784 #[test]
785 fn numeric_leading_dot_with_separators() {
786 assert_debug_snapshot!(lex(".1_2 .5_5 .1_2e3"), @r#"
787 [
788 ".1_2" @ Literal { kind: Numeric { empty_exponent_start: None, trailing_junk_start: 4 } },
789 " " @ Whitespace,
790 ".5_5" @ Literal { kind: Numeric { empty_exponent_start: None, trailing_junk_start: 4 } },
791 " " @ Whitespace,
792 ".1_2e3" @ Literal { kind: Numeric { empty_exponent_start: None, trailing_junk_start: 6 } },
793 ]
794 "#)
795 }
796
797 #[test]
798 fn numeric_exponent_underscore_after_sign() {
799 assert_debug_snapshot!(lex("1e+_2 1e-_2 1.0e+_2 .1e+_2"), @r#"
800 [
801 "1e" @ Literal { kind: Numeric { empty_exponent_start: Some(1), trailing_junk_start: 2 } },
802 "+" @ Plus,
803 "_2" @ Ident,
804 " " @ Whitespace,
805 "1e" @ Literal { kind: Numeric { empty_exponent_start: Some(1), trailing_junk_start: 2 } },
806 "-" @ Minus,
807 "_2" @ Ident,
808 " " @ Whitespace,
809 "1.0e" @ Literal { kind: Numeric { empty_exponent_start: Some(3), trailing_junk_start: 4 } },
810 "+" @ Plus,
811 "_2" @ Ident,
812 " " @ Whitespace,
813 ".1e" @ Literal { kind: Numeric { empty_exponent_start: Some(2), trailing_junk_start: 3 } },
814 "+" @ Plus,
815 "_2" @ Ident,
816 ]
817 "#)
818 }
819
820 #[test]
821 fn select_with_period() {
822 assert_debug_snapshot!(lex(r#"
823select public.users;
824"#))
825 }
826
827 #[test]
828 fn bitstring() {
829 assert_debug_snapshot!(lex(r#"
830B'1001'
831b'1001'
832X'1FF'
833x'1FF'
834"#))
835 }
836
837 #[test]
838 fn national_character_string() {
839 assert_debug_snapshot!(lex("N'foo' n'bar' numeric'1'"), @r#"
840 [
841 "N'foo'" @ Literal { kind: NationalStr { terminated: true } },
842 " " @ Whitespace,
843 "n'bar'" @ Literal { kind: NationalStr { terminated: true } },
844 " " @ Whitespace,
845 "numeric" @ Ident,
846 "'1'" @ Literal { kind: Str { terminated: true } },
847 ]
848 "#);
849 }
850
851 #[test]
852 fn ident_prefix_then_string_is_consistent() {
853 assert_debug_snapshot!(
854 lex("N1'foo' E1'foo' B1'foo' X1'foo' U1'foo' uuid'00000000'"),
855 @r#"
856 [
857 "N1" @ Ident,
858 "'foo'" @ Literal { kind: Str { terminated: true } },
859 " " @ Whitespace,
860 "E1" @ Ident,
861 "'foo'" @ Literal { kind: Str { terminated: true } },
862 " " @ Whitespace,
863 "B1" @ Ident,
864 "'foo'" @ Literal { kind: Str { terminated: true } },
865 " " @ Whitespace,
866 "X1" @ Ident,
867 "'foo'" @ Literal { kind: Str { terminated: true } },
868 " " @ Whitespace,
869 "U1" @ Ident,
870 "'foo'" @ Literal { kind: Str { terminated: true } },
871 " " @ Whitespace,
872 "uuid" @ Ident,
873 "'00000000'" @ Literal { kind: Str { terminated: true } },
874 ]
875 "#);
876 }
877
878 #[test]
879 fn string() {
880 assert_debug_snapshot!(lex(r#"
881'Dianne''s horse'
882
883select 'foo ''
884bar';
885
886select 'foooo'
887 'bar';
888
889
890'foo \\ \n \tbar'
891
892'forgot to close the string
893"#))
894 }
895
896 #[test]
897 fn params() {
898 assert_debug_snapshot!(lex(r#"
899select $1 + $2;
900
901select $1123123123123;
902
903select $;
904"#))
905 }
906
907 #[test]
908 fn string_with_escapes() {
909 assert_debug_snapshot!(lex(r#"
912E'foo'
913
914e'bar'
915
916e'\b\f\n\r\t'
917
918e'\0\11\777'
919
920e'\x0\x11\xFF'
921
922e'\uAAAA \UFFFFFFFF'
923
924"#))
925 }
926
927 #[test]
928 fn escape_string_with_backslash_escaped_quote() {
929 assert_debug_snapshot!(lex(r"E'foo\'bar'"), @r#"
930 [
931 "E'foo\\'bar'" @ Literal { kind: EscStr { terminated: true } },
932 ]
933 "#);
934 }
935
936 #[test]
937 fn escape_string_with_escaped_terminal_quote_is_unterminated() {
938 assert_debug_snapshot!(lex(r"E'foo\';"), @r#"
939 [
940 "E'foo\\';" @ Literal { kind: EscStr { terminated: false } },
941 ]
942 "#);
943 }
944
945 #[test]
946 fn escape_string_with_even_backslashes_before_quote_is_terminated() {
947 assert_debug_snapshot!(lex(r"E'foo\\'"), @r#"
948 [
949 "E'foo\\\\'" @ Literal { kind: EscStr { terminated: true } },
950 ]
951 "#);
952 }
953
954 #[test]
955 fn string_unicode_escape() {
956 assert_debug_snapshot!(lex(r#"
959U&"d\0061t\+000061"
960
961U&"\0441\043B\043E\043D"
962
963u&'\0441\043B'
964
965U&"d!0061t!+000061" UESCAPE '!'
966"#))
967 }
968
969 #[test]
970 fn quoted_ident() {
971 assert_debug_snapshot!(lex(r#"
972"hello &1 -world";
973
974
975"hello-world
976"#))
977 }
978
979 #[test]
980 fn quoted_ident_with_escape_quote() {
981 assert_debug_snapshot!(lex(r#"
982"foo "" bar"
983"#))
984 }
985
986 #[test]
987 fn dollar_quoted_string() {
988 assert_debug_snapshot!(lex("$$$$"), @r#"
989 [
990 "$$$$" @ Literal { kind: DollarQuotedString { terminated: true } },
991 ]
992 "#);
993 }
994
995 #[test]
996 fn tagged_dollar_quote_requires_leading_dollar() {
997 assert_debug_snapshot!(lex("select $foo$abcfoo$def$foo$;"), @r#"
998 [
999 "select" @ Ident,
1000 " " @ Whitespace,
1001 "$foo$abcfoo$def$foo$" @ Literal { kind: DollarQuotedString { terminated: true } },
1002 ";" @ Semi,
1003 ]
1004 "#);
1005 }
1006
1007 #[test]
1008 fn unclosed_dollar_tag_does_not_swallow_rest_of_input() {
1009 assert_debug_snapshot!(lex("select $x;\ndrop table users;"), @r#"
1010 [
1011 "select" @ Ident,
1012 " " @ Whitespace,
1013 "$x" @ PositionalParam { trailing_junk_start: 1 },
1014 ";" @ Semi,
1015 "\n" @ Whitespace,
1016 "drop" @ Ident,
1017 " " @ Whitespace,
1018 "table" @ Ident,
1019 " " @ Whitespace,
1020 "users" @ Ident,
1021 ";" @ Semi,
1022 ]
1023 "#);
1024 }
1025
1026 #[test]
1027 fn bom_at_start() {
1028 assert_debug_snapshot!(lex("\u{feff}select 1;"), @r#"
1029 [
1030 "\u{feff}" @ Whitespace,
1031 "select" @ Ident,
1032 " " @ Whitespace,
1033 "1" @ Literal { kind: Int { base: Decimal, empty_int: false, trailing_junk_start: 1 } },
1034 ";" @ Semi,
1035 ]
1036 "#);
1037 }
1038
1039 #[test]
1040 fn bom_after_start_is_an_ident_char() {
1041 assert_debug_snapshot!(lex("select 1;\n\u{feff}select 2;"), @r#"
1042 [
1043 "select" @ Ident,
1044 " " @ Whitespace,
1045 "1" @ Literal { kind: Int { base: Decimal, empty_int: false, trailing_junk_start: 1 } },
1046 ";" @ Semi,
1047 "\n" @ Whitespace,
1048 "\u{feff}select" @ Ident,
1049 " " @ Whitespace,
1050 "2" @ Literal { kind: Int { base: Decimal, empty_int: false, trailing_junk_start: 1 } },
1051 ";" @ Semi,
1052 ]
1053 "#);
1054 }
1055
1056 #[test]
1057 fn ident_non_ascii_above_latin1() {
1058 assert_debug_snapshot!(lex("ẞ Ā 漢字 𐐷"), @r#"
1059 [
1060 "ẞ" @ Ident,
1061 " " @ Whitespace,
1062 "Ā" @ Ident,
1063 " " @ Whitespace,
1064 "漢字" @ Ident,
1065 " " @ Whitespace,
1066 "𐐷" @ Ident,
1067 ]
1068 "#);
1069 }
1070}