1use crate::token::Token;
2
3pub const POWQL_KEYWORDS: &[&str] = &[
7 "abs",
8 "add",
9 "alter",
10 "and",
11 "as",
12 "asc",
13 "auto",
14 "avg",
15 "begin",
16 "between",
17 "case",
18 "cast",
19 "ceil",
20 "column",
21 "commit",
22 "concat",
23 "conflict",
24 "count",
25 "cross",
26 "date_add",
27 "date_diff",
28 "default",
29 "delete",
30 "dense_rank",
31 "desc",
32 "describe",
33 "distinct",
34 "drop",
35 "else",
36 "end",
37 "exists",
38 "explain",
39 "extract",
40 "false",
41 "filter",
42 "floor",
43 "group",
44 "having",
45 "in",
46 "index",
47 "inner",
48 "insert",
49 "is",
50 "join",
51 "json_type",
52 "left",
53 "length",
54 "let",
55 "like",
56 "limit",
57 "link",
58 "lower",
59 "match",
60 "materialize",
61 "materialized",
62 "max",
63 "min",
64 "multi",
65 "not",
66 "now",
67 "null",
68 "offset",
69 "on",
70 "or",
71 "order",
72 "outer",
73 "over",
74 "partition",
75 "pow",
76 "rank",
77 "refresh",
78 "required",
79 "returning",
80 "right",
81 "rollback",
82 "round",
83 "row_number",
84 "schema",
85 "select",
86 "sqrt",
87 "substring",
88 "sum",
89 "then",
90 "transaction",
91 "trim",
92 "true",
93 "type",
94 "union",
95 "unique",
96 "update",
97 "upper",
98 "upsert",
99 "view",
100 "when",
101];
102
103const MAX_STRING_LITERAL: usize = 16 * 1024 * 1024;
106
107#[derive(Debug)]
108pub struct LexError {
109 pub message: String,
110 pub position: usize,
111}
112
113impl std::fmt::Display for LexError {
114 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 write!(f, "at position {}: {}", self.position, self.message)
116 }
117}
118
119impl std::error::Error for LexError {}
120
121pub fn lex(input: &str) -> Result<Vec<Token>, LexError> {
135 let mut tokens = Vec::new();
136 let chars: Vec<char> = input.chars().collect();
137 let mut pos = 0;
138
139 while pos < chars.len() {
140 if chars[pos].is_whitespace() {
142 pos += 1;
143 continue;
144 }
145
146 if chars[pos] == '#' {
148 while pos < chars.len() && chars[pos] != '\n' {
149 pos += 1;
150 }
151 continue;
152 }
153
154 if chars[pos] == '.' && pos + 1 < chars.len() && chars[pos + 1] == '`' {
157 pos += 2; let start = pos;
159 while pos < chars.len() && chars[pos] != '`' {
160 pos += 1;
161 }
162 if pos >= chars.len() {
163 return Err(LexError {
164 message: "unterminated quoted identifier".into(),
165 position: pos,
166 });
167 }
168 let name: String = chars[start..pos].iter().collect();
169 if name.is_empty() {
170 return Err(LexError {
171 message: "empty quoted identifier".into(),
172 position: start,
173 });
174 }
175 pos += 1; tokens.push(Token::DotIdent(name));
177 continue;
178 }
179
180 if chars[pos] == '`' {
183 pos += 1; let start = pos;
185 while pos < chars.len() && chars[pos] != '`' {
186 pos += 1;
187 }
188 if pos >= chars.len() {
189 return Err(LexError {
190 message: "unterminated quoted identifier".into(),
191 position: pos,
192 });
193 }
194 let name: String = chars[start..pos].iter().collect();
195 if name.is_empty() {
196 return Err(LexError {
197 message: "empty quoted identifier".into(),
198 position: start,
199 });
200 }
201 pos += 1; tokens.push(Token::Ident(name));
203 continue;
204 }
205
206 if chars[pos] == '.'
208 && pos + 1 < chars.len()
209 && (chars[pos + 1].is_alphabetic() || chars[pos + 1] == '_')
210 {
211 pos += 1; let start = pos;
213 while pos < chars.len() && (chars[pos].is_alphanumeric() || chars[pos] == '_') {
214 pos += 1;
215 }
216 let name: String = chars[start..pos].iter().collect();
217 tokens.push(Token::DotIdent(name));
218 continue;
219 }
220
221 if chars[pos] == '$' {
223 pos += 1;
224 let start = pos;
225 while pos < chars.len() && (chars[pos].is_alphanumeric() || chars[pos] == '_') {
226 pos += 1;
227 }
228 let name: String = chars[start..pos].iter().collect();
229 tokens.push(Token::Param(name));
230 continue;
231 }
232
233 if chars[pos] == '"' {
235 pos += 1;
236 let mut s = String::new();
237 while pos < chars.len() && chars[pos] != '"' {
238 if chars[pos] == '\\' && pos + 1 < chars.len() {
239 match chars[pos + 1] {
240 '"' => {
241 s.push('"');
242 pos += 2;
243 }
244 '\\' => {
245 s.push('\\');
246 pos += 2;
247 }
248 'n' => {
249 s.push('\n');
250 pos += 2;
251 }
252 't' => {
253 s.push('\t');
254 pos += 2;
255 }
256 _ => {
257 s.push(chars[pos + 1]);
258 pos += 2;
259 }
260 }
261 } else {
262 s.push(chars[pos]);
263 pos += 1;
264 }
265 }
266 if pos >= chars.len() {
267 return Err(LexError {
268 message: "unterminated string".into(),
269 position: pos,
270 });
271 }
272 pos += 1; if s.len() > MAX_STRING_LITERAL {
274 return Err(LexError {
275 message: format!(
276 "string literal exceeds maximum size of {}MB",
277 MAX_STRING_LITERAL / (1024 * 1024)
278 ),
279 position: pos,
280 });
281 }
282 tokens.push(Token::StringLit(s));
283 continue;
284 }
285
286 if chars[pos].is_ascii_digit()
288 || (chars[pos] == '-' && pos + 1 < chars.len() && chars[pos + 1].is_ascii_digit())
289 {
290 let start = pos;
291 if chars[pos] == '-' {
292 pos += 1;
293 }
294 while pos < chars.len() && chars[pos].is_ascii_digit() {
295 pos += 1;
296 }
297 if pos < chars.len()
298 && chars[pos] == '.'
299 && pos + 1 < chars.len()
300 && chars[pos + 1].is_ascii_digit()
301 {
302 pos += 1;
303 while pos < chars.len() && chars[pos].is_ascii_digit() {
304 pos += 1;
305 }
306 let s: String = chars[start..pos].iter().collect();
307 let value = s.parse::<f64>().map_err(|_| LexError {
308 message: format!("float literal out of range: {s}"),
309 position: start,
310 })?;
311 tokens.push(Token::FloatLit(value));
312 } else {
313 let s: String = chars[start..pos].iter().collect();
314 let value = s.parse::<i64>().map_err(|_| LexError {
315 message: format!("integer literal out of range for i64: {s}"),
316 position: start,
317 })?;
318 tokens.push(Token::IntLit(value));
319 }
320 continue;
321 }
322
323 if chars[pos].is_alphabetic() || chars[pos] == '_' {
325 let start = pos;
326 while pos < chars.len() && (chars[pos].is_alphanumeric() || chars[pos] == '_') {
327 pos += 1;
328 }
329 let word: String = chars[start..pos].iter().collect();
330 let token = match word.as_str() {
331 "type" => Token::Type,
332 "filter" => Token::Filter,
333 "order" => Token::Order,
334 "limit" => Token::Limit,
335 "offset" => Token::Offset,
336 "insert" => Token::Insert,
337 "update" => Token::Update,
338 "delete" => Token::Delete,
339 "default" => Token::Default,
340 "upsert" => Token::Upsert,
341 "returning" => Token::Returning,
342 "conflict" => Token::Conflict,
343 "select" => Token::Select,
344 "required" => Token::Required,
345 "multi" => Token::Multi,
346 "link" => Token::Link,
347 "index" => Token::Index,
348 "unique" => Token::Unique,
349 "on" => Token::On,
350 "asc" => Token::Asc,
351 "auto" => Token::Auto,
352 "desc" => Token::Desc,
353 "and" => Token::And,
354 "or" => Token::Or,
355 "not" => Token::Not,
356 "exists" => Token::Exists,
357 "let" => Token::Let,
358 "as" => Token::As,
359 "match" => Token::Match,
360 "group" => Token::Group,
361 "join" => Token::Join,
362 "inner" => Token::Inner,
363 "left" => Token::LeftKw,
364 "right" => Token::RightKw,
365 "outer" => Token::Outer,
366 "cross" => Token::Cross,
367 "transaction" => Token::Transaction,
368 "begin" => Token::Begin,
369 "commit" => Token::Commit,
370 "rollback" => Token::Rollback,
371 "view" => Token::View,
372 "materialized" => Token::Materialized,
373 "materialize" => Token::Materialized,
374 "refresh" => Token::Refresh,
375 "union" => Token::Union,
376 "having" => Token::Having,
377 "distinct" => Token::Distinct,
378 "in" => Token::In,
379 "between" => Token::Between,
380 "like" => Token::Like,
381 "count" => Token::Count,
382 "avg" => Token::Avg,
383 "sum" => Token::Sum,
384 "min" => Token::Min,
385 "max" => Token::Max,
386 "is" => Token::Is,
387 "null" => Token::Null,
388 "upper" => Token::Upper,
389 "lower" => Token::Lower,
390 "length" => Token::Length,
391 "trim" => Token::Trim,
392 "substring" => Token::Substring,
393 "concat" => Token::Concat,
394 "abs" => Token::Abs,
395 "round" => Token::Round,
396 "ceil" => Token::Ceil,
397 "floor" => Token::Floor,
398 "sqrt" => Token::Sqrt,
399 "pow" => Token::Pow,
400 "now" => Token::Now,
401 "extract" => Token::Extract,
402 "date_add" => Token::DateAdd,
403 "date_diff" => Token::DateDiff,
404 "json_type" => Token::JsonType,
405 "cast" => Token::Cast,
406 "case" => Token::Case,
407 "when" => Token::When,
408 "then" => Token::Then,
409 "else" => Token::Else,
410 "end" => Token::End,
411 "over" => Token::Over,
412 "partition" => Token::Partition,
413 "row_number" => Token::RowNumber,
414 "rank" => Token::Rank,
415 "dense_rank" => Token::DenseRank,
416 "alter" => Token::Alter,
417 "drop" => Token::Drop,
418 "add" => Token::Add,
419 "column" => Token::Column,
420 "explain" => Token::Explain,
421 "schema" => Token::Schema,
422 "describe" => Token::Describe,
423 "true" => Token::BoolLit(true),
424 "false" => Token::BoolLit(false),
425 _ => Token::Ident(word),
426 };
427 tokens.push(token);
428 continue;
429 }
430
431 if pos + 1 < chars.len() {
433 let two: String = chars[pos..pos + 2].iter().collect();
434 match two.as_str() {
435 ":=" => {
436 tokens.push(Token::Assign);
437 pos += 2;
438 continue;
439 }
440 "->" => {
441 tokens.push(Token::Arrow);
442 pos += 2;
443 continue;
444 }
445 "!=" => {
446 tokens.push(Token::Neq);
447 pos += 2;
448 continue;
449 }
450 "<=" => {
451 tokens.push(Token::Lte);
452 pos += 2;
453 continue;
454 }
455 ">=" => {
456 tokens.push(Token::Gte);
457 pos += 2;
458 continue;
459 }
460 "??" => {
461 tokens.push(Token::Coalesce);
462 pos += 2;
463 continue;
464 }
465 _ => {}
466 }
467 }
468
469 let token = match chars[pos] {
471 '=' => Token::Eq,
472 '<' => Token::Lt,
473 '>' => Token::Gt,
474 '|' => Token::Pipe,
475 '+' => Token::Plus,
476 '-' => Token::Minus,
477 '*' => Token::Star,
478 '/' => Token::Slash,
479 '{' => Token::LBrace,
480 '}' => Token::RBrace,
481 '(' => Token::LParen,
482 ')' => Token::RParen,
483 ',' => Token::Comma,
484 ':' => Token::Colon,
485 '.' => Token::Dot,
486 c => {
487 return Err(LexError {
488 message: format!("unexpected character: {c}"),
489 position: pos,
490 })
491 }
492 };
493 tokens.push(token);
494 pos += 1;
495 }
496
497 tokens.push(Token::Eof);
498 Ok(tokens)
499}
500
501pub fn split_statements(input: &str) -> Vec<&str> {
511 #[derive(PartialEq)]
512 enum State {
513 Normal,
514 InString,
515 InComment,
516 }
517
518 let mut out = Vec::new();
519 let mut start = 0usize;
520 let mut state = State::Normal;
521 let mut chars = input.char_indices();
522
523 while let Some((i, c)) = chars.next() {
524 match state {
525 State::Normal => match c {
526 ';' => {
527 let seg = input[start..i].trim();
528 if !seg.is_empty() {
529 out.push(seg);
530 }
531 start = i + 1; }
533 '"' => state = State::InString,
534 '#' => state = State::InComment,
535 _ => {}
536 },
537 State::InString => match c {
538 '\\' => {
541 chars.next();
542 }
543 '"' => state = State::Normal,
544 _ => {}
545 },
546 State::InComment => {
547 if c == '\n' {
548 state = State::Normal;
549 }
550 }
551 }
552 }
553
554 let seg = input[start..].trim();
555 if !seg.is_empty() {
556 out.push(seg);
557 }
558 out
559}
560
561#[cfg(test)]
562mod tests {
563 use super::*;
564 use crate::token::Token;
565
566 #[test]
567 fn test_lex_simple_query() {
568 let tokens = lex("User filter .age > 30").unwrap();
569 assert_eq!(
570 tokens,
571 vec![
572 Token::Ident("User".into()),
573 Token::Filter,
574 Token::DotIdent("age".into()),
575 Token::Gt,
576 Token::IntLit(30),
577 Token::Eof,
578 ]
579 );
580 }
581
582 #[test]
583 fn test_lex_projection() {
584 let tokens = lex("User { name, email }").unwrap();
585 assert_eq!(
586 tokens,
587 vec![
588 Token::Ident("User".into()),
589 Token::LBrace,
590 Token::Ident("name".into()),
591 Token::Comma,
592 Token::Ident("email".into()),
593 Token::RBrace,
594 Token::Eof,
595 ]
596 );
597 }
598
599 #[test]
600 fn test_lex_insert() {
601 let tokens = lex(r#"insert User { name := "Alice", age := 30 }"#).unwrap();
602 assert_eq!(
603 tokens,
604 vec![
605 Token::Insert,
606 Token::Ident("User".into()),
607 Token::LBrace,
608 Token::Ident("name".into()),
609 Token::Assign,
610 Token::StringLit("Alice".into()),
611 Token::Comma,
612 Token::Ident("age".into()),
613 Token::Assign,
614 Token::IntLit(30),
615 Token::RBrace,
616 Token::Eof,
617 ]
618 );
619 }
620
621 #[test]
622 fn test_lex_params() {
623 let tokens = lex("User filter .age > $min_age").unwrap();
624 assert_eq!(
625 tokens,
626 vec![
627 Token::Ident("User".into()),
628 Token::Filter,
629 Token::DotIdent("age".into()),
630 Token::Gt,
631 Token::Param("min_age".into()),
632 Token::Eof,
633 ]
634 );
635 }
636
637 #[test]
638 fn test_lex_string_with_escapes() {
639 let tokens = lex(r#""hello \"world\"""#).unwrap();
640 assert_eq!(
641 tokens,
642 vec![Token::StringLit("hello \"world\"".into()), Token::Eof,]
643 );
644 }
645
646 #[test]
647 fn test_lex_aggregation() {
648 let tokens = lex("count(User)").unwrap();
649 assert_eq!(
650 tokens,
651 vec![
652 Token::Count,
653 Token::LParen,
654 Token::Ident("User".into()),
655 Token::RParen,
656 Token::Eof,
657 ]
658 );
659 }
660
661 #[test]
665 fn test_lex_backtick_identifier() {
666 let tokens = lex("type Post { `type`: str }").unwrap();
668 assert_eq!(
669 tokens,
670 vec![
671 Token::Type,
672 Token::Ident("Post".into()),
673 Token::LBrace,
674 Token::Ident("type".into()),
675 Token::Colon,
676 Token::Ident("str".into()),
677 Token::RBrace,
678 Token::Eof,
679 ]
680 );
681 }
682
683 #[test]
684 fn test_lex_backtick_dot_identifier() {
685 let tokens = lex("Post filter .`type` = \"x\"").unwrap();
687 assert_eq!(
688 tokens,
689 vec![
690 Token::Ident("Post".into()),
691 Token::Filter,
692 Token::DotIdent("type".into()),
693 Token::Eq,
694 Token::StringLit("x".into()),
695 Token::Eof,
696 ]
697 );
698 }
699
700 #[test]
701 fn test_lex_backtick_allows_spaces_in_identifier() {
702 let tokens = lex("`full name`").unwrap();
703 assert_eq!(tokens, vec![Token::Ident("full name".into()), Token::Eof]);
704 }
705
706 #[test]
707 fn test_lex_unterminated_backtick_errors() {
708 let err = lex("`type").expect_err("must error");
709 assert!(err.message.contains("unterminated quoted identifier"));
710 }
711
712 #[test]
713 fn test_lex_empty_backtick_errors() {
714 let err = lex("``").expect_err("must error");
715 assert!(err.message.contains("empty quoted identifier"));
716 }
717
718 #[test]
719 fn test_lex_schema_and_describe_keywords() {
720 assert_eq!(lex("schema").unwrap(), vec![Token::Schema, Token::Eof]);
721 assert_eq!(
722 lex("describe Post").unwrap(),
723 vec![Token::Describe, Token::Ident("Post".into()), Token::Eof]
724 );
725 }
726
727 #[test]
728 fn test_lex_intlit_overflow_returns_err() {
729 let err = lex("4444444441111111144444").expect_err("must error, not panic");
731 assert!(
732 err.message.contains("integer literal out of range"),
733 "unexpected message: {}",
734 err.message
735 );
736 assert_eq!(err.position, 0);
737 }
738
739 #[test]
743 fn test_lex_fuzz_repro_issue_24() {
744 let input = "as\t\t\t\t\t\t\t\t\t\t\t\t\t44444444411111114444\t\t\t\t\t\t";
745 let err = lex(input).expect_err("fuzz reproducer must now error, not panic");
746 assert!(err.message.contains("integer literal"));
747 }
748
749 #[test]
752 fn test_split_top_level_semicolons() {
753 assert_eq!(
754 split_statements("insert A { a := 1 }; insert B { b := 2 }"),
755 vec!["insert A { a := 1 }", "insert B { b := 2 }"]
756 );
757 }
758
759 #[test]
760 fn test_split_semicolon_in_string_not_split() {
761 assert_eq!(
763 split_statements(r#"insert Note { body := "hello; world" }"#),
764 vec![r#"insert Note { body := "hello; world" }"#]
765 );
766 }
767
768 #[test]
769 fn test_split_escaped_quote_then_semicolon() {
770 let input = r#"insert A { v := "a\"; b" }; insert B { c := 1 }"#;
774 assert_eq!(
775 split_statements(input),
776 vec![r#"insert A { v := "a\"; b" }"#, "insert B { c := 1 }"]
777 );
778 }
779
780 #[test]
781 fn test_split_backslash_consumes_any_char() {
782 let input = r#"insert A { v := "\\" }; x"#;
785 assert_eq!(
786 split_statements(input),
787 vec![r#"insert A { v := "\\" }"#, "x"]
788 );
789 }
790
791 #[test]
792 fn test_split_semicolon_in_comment_not_split() {
793 let input = "insert A { a := 1 } # trailing; comment\n; insert B { b := 2 }";
794 assert_eq!(
795 split_statements(input),
796 vec![
797 "insert A { a := 1 } # trailing; comment",
798 "insert B { b := 2 }"
799 ]
800 );
801 }
802
803 #[test]
804 fn test_split_drops_empty_segments() {
805 assert_eq!(split_statements("; A ;; B ;\n\n"), vec!["A", "B"]);
807 }
808
809 #[test]
810 fn test_split_no_semicolon_backcompat() {
811 assert_eq!(split_statements("count(User)"), vec!["count(User)"]);
814 assert!(split_statements(" ").is_empty());
815 }
816
817 #[test]
818 fn test_split_unterminated_string_tail() {
819 let input = r#"insert A { a := 1 }; insert B { b := "oops"#;
821 assert_eq!(
822 split_statements(input),
823 vec!["insert A { a := 1 }", r#"insert B { b := "oops"#]
824 );
825 }
826
827 #[test]
828 fn test_split_multiline_string_with_semicolon() {
829 let input = "insert A { body := \"line1;\nline2\" }; insert B { b := 2 }";
830 assert_eq!(
831 split_statements(input),
832 vec![
833 "insert A { body := \"line1;\nline2\" }",
834 "insert B { b := 2 }"
835 ]
836 );
837 }
838}