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