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