1use sqlparser::dialect::SQLiteDialect;
2use sqlparser::parser::Parser;
3
4use crate::error::ParseError;
5
6pub fn parse_sql(sql: &str) -> Result<Vec<sqlparser::ast::Statement>, ParseError> {
7 let dialect = SQLiteDialect {};
8 let numbered = number_placeholders(sql);
15 let preprocessed =
16 preprocess_update_limit(&preprocess_bitwise_shifts(&preprocess_bitwise_not(
17 &preprocess_is_truth_family(&preprocess_fts5_match(&preprocess_pragma(&numbered))),
18 )));
19 if is_vacuum(&preprocessed) {
20 return Ok(vec![make_pragma_statement("__vacuum", None)]);
21 }
22 if let Some(arg) = strip_keyword(&preprocessed, "REINDEX") {
23 return Ok(vec![make_pragma_statement("__reindex", Some(&arg))]);
24 }
25 if let Some(arg) = strip_keyword(&preprocessed, "ANALYZE") {
26 return Ok(vec![make_pragma_statement("__analyze", Some(&arg))]);
27 }
28 if let Some(stmt) = parse_trigger_statement(&preprocessed) {
29 return Ok(vec![stmt]);
30 }
31 if let Some(stmt) = parse_detach_statement(&preprocessed) {
32 return Ok(vec![stmt]);
33 }
34 if let Some(stmt) = parse_create_virtual_table(&preprocessed) {
35 return Ok(vec![stmt]);
36 }
37 let statements = Parser::parse_sql(&dialect, &preprocessed)?;
38 Ok(statements)
39}
40
41fn number_placeholders(sql: &str) -> String {
46 let chars: Vec<char> = sql.chars().collect();
47 let mut out = String::with_capacity(sql.len() + 8);
48 let mut i = 0;
49 let mut n: u32 = 0;
50 while i < chars.len() {
51 let c = chars[i];
52 match c {
53 '\'' | '"' | '`' => {
56 let quote = c;
57 out.push(c);
58 i += 1;
59 while i < chars.len() {
60 let d = chars[i];
61 out.push(d);
62 i += 1;
63 if d == quote {
64 if (quote == '\'' || quote == '"') && i < chars.len() && chars[i] == quote {
65 out.push(quote);
66 i += 1;
67 } else {
68 break;
69 }
70 }
71 }
72 }
73 '[' => {
74 out.push(c);
75 i += 1;
76 while i < chars.len() {
77 let d = chars[i];
78 out.push(d);
79 i += 1;
80 if d == ']' {
81 break;
82 }
83 }
84 }
85 '-' if i + 1 < chars.len() && chars[i + 1] == '-' => {
86 while i < chars.len() && chars[i] != '\n' {
87 out.push(chars[i]);
88 i += 1;
89 }
90 }
91 '/' if i + 1 < chars.len() && chars[i + 1] == '*' => {
92 out.push('/');
93 out.push('*');
94 i += 2;
95 while i < chars.len() {
96 if chars[i] == '*' && i + 1 < chars.len() && chars[i + 1] == '/' {
97 out.push('*');
98 out.push('/');
99 i += 2;
100 break;
101 }
102 out.push(chars[i]);
103 i += 1;
104 }
105 }
106 '?' => {
107 if i + 1 < chars.len() && chars[i + 1].is_ascii_digit() {
109 out.push('?');
110 i += 1;
111 } else {
112 n += 1;
113 out.push('?');
114 out.push_str(&n.to_string());
115 i += 1;
116 }
117 }
118 _ => {
119 out.push(c);
120 i += 1;
121 }
122 }
123 }
124 out
125}
126
127fn parse_create_virtual_table(sql: &str) -> Option<sqlparser::ast::Statement> {
134 let trimmed = sql.trim().trim_end_matches(';').trim();
135 let upper = trimmed.to_uppercase();
136 let prefix = "CREATE VIRTUAL TABLE";
137 if !upper.starts_with(prefix) {
138 return None;
139 }
140 let rest = trimmed[prefix.len()..].trim_start();
141 let upper_rest = rest.to_uppercase();
142 let (rest, if_not_exists) = if upper_rest.starts_with("IF NOT EXISTS") {
143 (rest["IF NOT EXISTS".len()..].trim_start(), true)
144 } else {
145 (rest, false)
146 };
147
148 let name_end = rest.find(char::is_whitespace).unwrap_or(rest.len());
150 let name =
151 rest[..name_end].trim_matches(|c: char| c == '"' || c == '`' || c == '[' || c == ']');
152 if name.is_empty() {
153 return None;
154 }
155 let after_name = rest[name_end..].trim_start();
156
157 let upper_after = after_name.to_uppercase();
159 if !upper_after.starts_with("USING") {
160 return None;
161 }
162 let after_using = after_name["USING".len()..]
163 .trim_start()
164 .trim_end_matches(';')
165 .trim();
166 let (module, raw_args) = match after_using.find('(') {
169 Some(paren) => {
170 let close = after_using.rfind(')')?;
171 if close <= paren {
172 return None;
173 }
174 (after_using[..paren].trim(), &after_using[paren + 1..close])
175 }
176 None => (after_using, ""),
177 };
178 if module.is_empty() {
179 return None;
180 }
181
182 let encoded = format!(
185 "{}|{}|{}|{}",
186 if if_not_exists { "1" } else { "0" },
187 name,
188 module,
189 raw_args
190 );
191 Some(make_pragma_statement(
192 "__create_virtual_table",
193 Some(&encoded),
194 ))
195}
196
197fn parse_trigger_statement(sql: &str) -> Option<sqlparser::ast::Statement> {
198 let upper = sql.trim().to_uppercase();
199 if upper.starts_with("CREATE TRIGGER") || upper.starts_with("CREATE TRIGGER") {
200 return parse_create_trigger(sql.trim());
201 }
202 if upper.starts_with("DROP TRIGGER") {
203 return parse_drop_trigger(sql.trim());
204 }
205 None
206}
207
208fn parse_create_trigger(sql: &str) -> Option<sqlparser::ast::Statement> {
209 let original_sql = sql.trim().trim_end_matches(';').trim();
210 let upper = original_sql.to_uppercase();
211 let tokens: Vec<&str> = upper.split_whitespace().collect();
212
213 let mut pos = 2; let mut if_not_exists = false;
215 if tokens.get(pos) == Some(&"IF")
216 && tokens.get(pos + 1) == Some(&"NOT")
217 && tokens.get(pos + 2) == Some(&"EXISTS")
218 {
219 if_not_exists = true;
220 pos += 3;
221 }
222
223 let name = tokens.get(pos)?.to_string();
224 pos += 1;
225
226 let timing = match tokens.get(pos).copied()? {
227 "BEFORE" => {
228 pos += 1;
229 "BEFORE"
230 }
231 "AFTER" => {
232 pos += 1;
233 "AFTER"
234 }
235 "INSTEAD" => {
236 if tokens.get(pos + 1) == Some(&"OF") {
237 pos += 2;
238 "INSTEAD OF"
239 } else {
240 return None;
241 }
242 }
243 _ => return None,
244 };
245
246 let event = match tokens.get(pos).copied()? {
247 "INSERT" => {
248 pos += 1;
249 "INSERT"
250 }
251 "UPDATE" => {
252 pos += 1;
253 "UPDATE"
254 }
255 "DELETE" => {
256 pos += 1;
257 "DELETE"
258 }
259 _ => return None,
260 };
261
262 if tokens.get(pos) != Some(&"ON") {
263 return None;
264 }
265 pos += 1;
266
267 let table_name = tokens.get(pos)?.to_string();
268 pos += 1;
269
270 if tokens.get(pos) == Some(&"FOR")
271 && tokens.get(pos + 1) == Some(&"EACH")
272 && tokens.get(pos + 2) == Some(&"ROW")
273 {
274 pos += 3;
275 }
276
277 let upper_sql = original_sql.to_uppercase();
279 let begin_idx = find_keyword_pos(&upper_sql, pos, &tokens, "BEGIN")?;
280
281 let when_condition = if tokens.get(pos) == Some(&"WHEN") {
282 let when_start = find_word_offset(original_sql, &tokens, pos + 1)?;
283 let when_end = begin_idx;
284 let cond = original_sql[when_start..when_end].trim().to_string();
285 Some(cond)
286 } else {
287 None
288 };
289
290 let body_start = begin_idx + "BEGIN".len();
291 let end_idx = upper_sql.rfind("END")?;
292 let body_sql = original_sql[body_start..end_idx].trim().to_string();
293
294 let encoded = format!(
295 "{}|{}|{}|{}|{}|{}|{}",
296 name,
297 table_name,
298 timing,
299 event,
300 if if_not_exists { "1" } else { "0" },
301 when_condition.as_deref().unwrap_or(""),
302 body_sql
303 );
304
305 Some(make_pragma_statement("__create_trigger", Some(&encoded)))
306}
307
308fn find_keyword_pos(
309 upper_sql: &str,
310 _start_token: usize,
311 _tokens: &[&str],
312 keyword: &str,
313) -> Option<usize> {
314 upper_sql.find(keyword)
315}
316
317fn find_word_offset(sql: &str, tokens: &[&str], token_idx: usize) -> Option<usize> {
318 let upper = sql.to_uppercase();
319 let mut search_start = 0;
320 for i in 0..token_idx {
321 let tok = tokens.get(i)?;
322 if let Some(pos) = upper[search_start..].find(tok) {
323 search_start += pos + tok.len();
324 }
325 }
326 while search_start < sql.len() && sql.as_bytes()[search_start] == b' ' {
327 search_start += 1;
328 }
329 Some(search_start)
330}
331
332fn parse_detach_statement(sql: &str) -> Option<sqlparser::ast::Statement> {
333 let trimmed = sql.trim().trim_end_matches(';').trim();
334 let upper = trimmed.to_uppercase();
335 if !upper.starts_with("DETACH") {
336 return None;
337 }
338 let upper_tokens: Vec<&str> = upper.split_whitespace().collect();
339 let orig_tokens: Vec<&str> = trimmed.split_whitespace().collect();
340 let mut pos = 1; if upper_tokens.get(pos) == Some(&"DATABASE") {
342 pos += 1;
343 }
344 let schema_name = orig_tokens.get(pos)?;
345 Some(make_pragma_statement("__detach", Some(schema_name)))
346}
347
348fn parse_drop_trigger(sql: &str) -> Option<sqlparser::ast::Statement> {
349 let trimmed = sql.trim().trim_end_matches(';').trim();
350 let upper = trimmed.to_uppercase();
351 let tokens: Vec<&str> = upper.split_whitespace().collect();
352
353 let mut pos = 2; let mut if_exists = false;
355 if tokens.get(pos) == Some(&"IF") && tokens.get(pos + 1) == Some(&"EXISTS") {
356 if_exists = true;
357 pos += 2;
358 }
359 let name = tokens.get(pos)?.to_string();
360 let encoded = format!("{}|{}", name, if if_exists { "1" } else { "0" });
361 Some(make_pragma_statement("__drop_trigger", Some(&encoded)))
362}
363
364fn is_vacuum(sql: &str) -> bool {
365 let trimmed = sql.trim().trim_end_matches(';').trim();
366 trimmed.eq_ignore_ascii_case("VACUUM")
367}
368
369fn strip_keyword(sql: &str, keyword: &str) -> Option<String> {
373 let trimmed = sql.trim().trim_end_matches(';').trim();
374 let upper = trimmed.to_uppercase();
375 if upper == keyword {
376 return Some(String::new());
377 }
378 let prefix = format!("{keyword} ");
379 if upper.starts_with(&prefix) {
380 return Some(trimmed[prefix.len()..].trim().to_string());
381 }
382 None
383}
384
385fn make_pragma_statement(name: &str, value: Option<&str>) -> sqlparser::ast::Statement {
386 use sqlparser::ast::{Ident, ObjectName, Value};
387 sqlparser::ast::Statement::Pragma {
388 name: ObjectName::from(vec![Ident::new(name)]),
389 value: value.map(|v| Value::SingleQuotedString(v.to_string())),
390 is_eq: value.is_some(),
391 }
392}
393
394fn preprocess_is_truth_family(sql: &str) -> String {
408 let upper_haystack = sql.to_ascii_uppercase();
413 if !upper_haystack.contains("IS TRUE")
414 && !upper_haystack.contains("IS FALSE")
415 && !upper_haystack.contains("IS NOT TRUE")
416 && !upper_haystack.contains("IS NOT FALSE")
417 {
418 return sql.to_string();
419 }
420
421 let bytes = sql.as_bytes();
422 let n = bytes.len();
423 let mut out = String::with_capacity(n);
424 let mut i = 0usize;
425
426 while i < n {
427 let ch = bytes[i] as char;
428 if ch == '\'' || ch == '"' {
430 let quote = ch;
431 out.push(ch);
432 i += 1;
433 while i < n {
434 let c = bytes[i] as char;
435 out.push(c);
436 i += 1;
437 if c == quote {
438 if i < n && (bytes[i] as char) == quote {
440 out.push(quote);
441 i += 1;
442 continue;
443 }
444 break;
445 }
446 }
447 continue;
448 }
449 if ch == '-' && i + 1 < n && bytes[i + 1] as char == '-' {
451 while i < n && bytes[i] as char != '\n' {
452 out.push(bytes[i] as char);
453 i += 1;
454 }
455 continue;
456 }
457
458 let id_start = i;
461 let id = if ch == '(' {
462 let start = i;
465 let mut depth = 1;
466 i += 1;
467 while i < n && depth > 0 {
468 let c = bytes[i] as char;
469 if c == '(' {
470 depth += 1;
471 } else if c == ')' {
472 depth -= 1;
473 }
474 i += 1;
475 }
476 if depth != 0 {
477 out.push(ch);
478 i = start + 1;
479 continue;
480 }
481 String::from_utf8_lossy(&bytes[start..i]).into_owned()
482 } else {
483 scan_identifier(bytes, &mut i)
484 };
485 if id.is_empty() {
486 out.push(ch);
487 i += 1;
488 continue;
489 }
490 let saved_i = i;
492 let mut j = i;
493 skip_ws(bytes, &mut j);
494 if !match_keyword(bytes, &mut j, "IS") {
495 out.push_str(&id);
497 continue;
498 }
499 skip_ws(bytes, &mut j);
500 let negated = match_keyword(bytes, &mut j, "NOT");
501 if negated {
502 skip_ws(bytes, &mut j);
503 }
504 let truthy = if match_keyword(bytes, &mut j, "TRUE") {
505 Some(true)
506 } else if match_keyword(bytes, &mut j, "FALSE") {
507 Some(false)
508 } else {
509 None
510 };
511 match truthy {
512 Some(true_or_false) => {
513 let cmp = if true_or_false ^ negated { "<>" } else { "=" };
518 let null_test = if negated { "IS NULL" } else { "IS NOT NULL" };
519 let combiner = if negated { "OR" } else { "AND" };
520 let expanded = format!("({id} {null_test} {combiner} {id} {cmp} 0)");
521 let _ = id_start;
522 out.push_str(&expanded);
523 i = j;
524 }
525 None => {
526 out.push_str(&id);
527 i = saved_i;
528 }
529 }
530 }
531
532 out
533}
534
535fn scan_identifier(bytes: &[u8], i: &mut usize) -> String {
536 let start = *i;
537 let n = bytes.len();
538 if start >= n {
539 return String::new();
540 }
541 let first = bytes[start] as char;
542 if first == '"' || first == '`' {
544 let quote = first;
545 *i += 1;
546 while *i < n {
547 let c = bytes[*i] as char;
548 *i += 1;
549 if c == quote {
550 break;
551 }
552 }
553 if *i < n && bytes[*i] as char == '.' {
555 *i += 1;
556 scan_unquoted_id_tail(bytes, i);
557 }
558 return String::from_utf8_lossy(&bytes[start..*i]).into_owned();
559 }
560 if !first.is_ascii_alphabetic() && first != '_' {
561 return String::new();
562 }
563 *i += 1;
564 while *i < n {
565 let c = bytes[*i] as char;
566 if c.is_ascii_alphanumeric() || c == '_' {
567 *i += 1;
568 } else {
569 break;
570 }
571 }
572 if *i < n && bytes[*i] as char == '.' {
574 *i += 1;
575 scan_unquoted_id_tail(bytes, i);
576 }
577 String::from_utf8_lossy(&bytes[start..*i]).into_owned()
578}
579
580fn scan_unquoted_id_tail(bytes: &[u8], i: &mut usize) {
581 let n = bytes.len();
582 while *i < n {
583 let c = bytes[*i] as char;
584 if c.is_ascii_alphanumeric() || c == '_' {
585 *i += 1;
586 } else {
587 break;
588 }
589 }
590}
591
592fn skip_ws(bytes: &[u8], i: &mut usize) {
593 let n = bytes.len();
594 while *i < n && (bytes[*i] as char).is_whitespace() {
595 *i += 1;
596 }
597}
598
599fn match_keyword(bytes: &[u8], i: &mut usize, kw: &str) -> bool {
600 let n = bytes.len();
601 let kw_bytes = kw.as_bytes();
602 if *i + kw_bytes.len() > n {
603 return false;
604 }
605 for (k, kb) in kw_bytes.iter().enumerate() {
606 let c = bytes[*i + k];
607 if c.to_ascii_uppercase() != *kb {
608 return false;
609 }
610 }
611 let after = *i + kw_bytes.len();
613 if after < n {
614 let c = bytes[after] as char;
615 if c.is_ascii_alphanumeric() || c == '_' {
616 return false;
617 }
618 }
619 *i = after;
620 true
621}
622
623fn preprocess_bitwise_shifts(sql: &str) -> String {
638 if !sql.contains("<<") && !sql.contains(">>") {
639 return sql.to_string();
640 }
641
642 let mut current = sql.to_string();
643 for _ in 0..8 {
644 let rewritten = rewrite_shifts_one_pass(¤t);
645 if rewritten == current {
646 break;
647 }
648 current = rewritten;
649 }
650 current
651}
652
653fn rewrite_shifts_one_pass(sql: &str) -> String {
654 let bytes = sql.as_bytes();
655 let n = bytes.len();
656 let mut out = String::with_capacity(n);
657 let mut i = 0usize;
658
659 while i < n {
660 let ch = bytes[i] as char;
661 if ch == '\'' || ch == '"' {
662 let quote = ch;
663 out.push(ch);
664 i += 1;
665 while i < n {
666 let c = bytes[i] as char;
667 out.push(c);
668 i += 1;
669 if c == quote {
670 if i < n && (bytes[i] as char) == quote {
671 out.push(quote);
672 i += 1;
673 continue;
674 }
675 break;
676 }
677 }
678 continue;
679 }
680 if ch == '-' && i + 1 < n && bytes[i + 1] as char == '-' {
681 while i < n && bytes[i] as char != '\n' {
682 out.push(bytes[i] as char);
683 i += 1;
684 }
685 continue;
686 }
687 if i + 2 <= n {
691 let pair = &sql[i..i + 2];
692 if pair == "<<" || pair == ">>" {
693 if let Some((lhs_start, lhs)) = trailing_atom(&out) {
694 let after_op = i + 2;
696 if let Some((rhs, consumed)) = leading_atom(&sql[after_op..]) {
697 let func = if pair == "<<" { "__shl" } else { "__shr" };
698 out.truncate(lhs_start);
699 out.push_str(&format!("{func}({lhs}, {rhs})"));
700 i = after_op + consumed;
701 continue;
702 }
703 }
704 }
705 }
706 out.push(ch);
707 i += 1;
708 }
709 out
710}
711
712fn trailing_atom(out: &str) -> Option<(usize, String)> {
724 let trimmed = out.trim_end();
725 let bytes = trimmed.as_bytes();
726 let n = bytes.len();
727 if n == 0 {
728 return None;
729 }
730 let last = bytes[n - 1] as char;
731
732 if last == ')' {
734 let mut depth = 0i32;
735 let mut start = n;
736 for k in (0..n).rev() {
737 let c = bytes[k] as char;
738 if c == ')' {
739 depth += 1;
740 } else if c == '(' {
741 depth -= 1;
742 if depth == 0 {
743 start = k;
744 break;
745 }
746 }
747 }
748 if depth != 0 {
749 return None;
750 }
751 let mut id_start = start;
754 while id_start > 0 {
755 let c = bytes[id_start - 1] as char;
756 if c.is_ascii_alphanumeric() || c == '_' {
757 id_start -= 1;
758 } else {
759 break;
760 }
761 }
762 let text = String::from_utf8_lossy(&bytes[id_start..n]).into_owned();
763 return Some((id_start, text));
764 }
765
766 if last.is_ascii_digit() || last.is_ascii_alphabetic() || last == '_' {
768 let mut start = n;
769 while start > 0 {
770 let c = bytes[start - 1] as char;
771 if c.is_ascii_alphanumeric() || c == '_' || c == '.' {
772 start -= 1;
773 } else {
774 break;
775 }
776 }
777 let mut k = start;
780 while k > 0 && (bytes[k - 1] as char).is_whitespace() {
781 k -= 1;
782 }
783 if k > 0 {
784 let prev = bytes[k - 1] as char;
785 let prev2 = if k >= 2 {
787 Some(bytes[k - 2] as char)
788 } else {
789 None
790 };
791 let prev_op_higher_prec =
792 matches!(prev, '+' | '-' | '*' | '/' | '%') || (prev == '|' && prev2 == Some('|'));
793 if prev_op_higher_prec {
794 return None;
795 }
796 }
797 let text = String::from_utf8_lossy(&bytes[start..n]).into_owned();
798 return Some((start, text));
799 }
800
801 None
802}
803
804fn leading_atom(s: &str) -> Option<(String, usize)> {
806 let bytes = s.as_bytes();
807 let n = bytes.len();
808 let mut i = 0;
809 while i < n && (bytes[i] as char).is_whitespace() {
810 i += 1;
811 }
812 if i >= n {
813 return None;
814 }
815 let start = i;
816 let first = bytes[i] as char;
817
818 if first == '(' {
820 let mut depth = 1;
821 i += 1;
822 while i < n && depth > 0 {
823 let c = bytes[i] as char;
824 if c == '(' {
825 depth += 1;
826 } else if c == ')' {
827 depth -= 1;
828 }
829 i += 1;
830 }
831 if depth != 0 {
832 return None;
833 }
834 let text = String::from_utf8_lossy(&bytes[start..i]).into_owned();
835 return Some((text, i));
836 }
837
838 if first.is_ascii_digit() {
841 while i < n && (bytes[i] as char).is_ascii_digit() {
842 i += 1;
843 }
844 let text = String::from_utf8_lossy(&bytes[start..i]).into_owned();
845 return Some((text, i));
846 }
847
848 if first.is_ascii_alphabetic() || first == '_' {
850 while i < n {
851 let c = bytes[i] as char;
852 if c.is_ascii_alphanumeric() || c == '_' || c == '.' {
853 i += 1;
854 } else {
855 break;
856 }
857 }
858 if i < n && (bytes[i] as char) == '(' {
860 let mut depth = 1;
861 i += 1;
862 while i < n && depth > 0 {
863 let c = bytes[i] as char;
864 if c == '(' {
865 depth += 1;
866 } else if c == ')' {
867 depth -= 1;
868 }
869 i += 1;
870 }
871 if depth != 0 {
872 return None;
873 }
874 }
875 let text = String::from_utf8_lossy(&bytes[start..i]).into_owned();
876 return Some((text, i));
877 }
878
879 None
880}
881
882fn preprocess_bitwise_not(sql: &str) -> String {
896 if !sql.contains('~') {
897 return sql.to_string();
898 }
899
900 let bytes = sql.as_bytes();
901 let n = bytes.len();
902 let mut out = String::with_capacity(n);
903 let mut i = 0usize;
904
905 while i < n {
906 let ch = bytes[i] as char;
907 if ch == '\'' || ch == '"' {
908 let quote = ch;
909 out.push(ch);
910 i += 1;
911 while i < n {
912 let c = bytes[i] as char;
913 out.push(c);
914 i += 1;
915 if c == quote {
916 if i < n && (bytes[i] as char) == quote {
917 out.push(quote);
918 i += 1;
919 continue;
920 }
921 break;
922 }
923 }
924 continue;
925 }
926 if ch == '-' && i + 1 < n && bytes[i + 1] as char == '-' {
927 while i < n && bytes[i] as char != '\n' {
928 out.push(bytes[i] as char);
929 i += 1;
930 }
931 continue;
932 }
933 if ch != '~' {
934 out.push(ch);
935 i += 1;
936 continue;
937 }
938
939 let prev_significant = out.trim_end().chars().next_back();
945 let is_prefix = match prev_significant {
946 None => true,
947 Some(c) => matches!(
948 c,
949 '(' | ',' | '+' | '-' | '*' | '/' | '%' | '=' | '<' | '>' | '!' | '|' | '&' | '~'
950 ),
951 } || {
952 let trimmed = out.trim_end();
955 let last_word: String = trimmed
956 .chars()
957 .rev()
958 .take_while(|c| c.is_ascii_alphabetic() || *c == '_')
959 .collect::<String>()
960 .chars()
961 .rev()
962 .collect();
963 matches!(
964 last_word.to_ascii_uppercase().as_str(),
965 "WHERE"
966 | "AND"
967 | "OR"
968 | "NOT"
969 | "ON"
970 | "BY"
971 | "CASE"
972 | "WHEN"
973 | "THEN"
974 | "ELSE"
975 | "SELECT"
976 | "VALUES"
977 | "RETURNING"
978 | "HAVING"
979 | "LIMIT"
980 | "OFFSET"
981 )
982 };
983
984 if !is_prefix {
985 out.push(ch);
986 i += 1;
987 continue;
988 }
989
990 i += 1;
992 skip_ws(bytes, &mut i);
993 if i >= n {
994 out.push('~');
995 continue;
996 }
997 let next_ch = bytes[i] as char;
998 if next_ch == '(' {
999 let start = i;
1001 let mut depth = 1;
1002 i += 1;
1003 while i < n && depth > 0 {
1004 let c = bytes[i] as char;
1005 if c == '(' {
1006 depth += 1;
1007 } else if c == ')' {
1008 depth -= 1;
1009 }
1010 i += 1;
1011 }
1012 let operand = &sql[start..i];
1013 out.push_str(&format!("__bnot({operand})"));
1014 continue;
1015 }
1016 if next_ch.is_ascii_digit() {
1019 let start = i;
1020 while i < n {
1021 let c = bytes[i] as char;
1022 if c.is_ascii_digit() || c == '.' {
1023 i += 1;
1024 } else {
1025 break;
1026 }
1027 }
1028 out.push_str(&format!("__bnot({})", &sql[start..i]));
1029 continue;
1030 }
1031 let id = scan_identifier(bytes, &mut i);
1033 if id.is_empty() {
1034 out.push('~');
1035 continue;
1036 }
1037 out.push_str(&format!("__bnot({id})"));
1038 }
1039
1040 out
1041}
1042
1043fn preprocess_update_limit(sql: &str) -> String {
1059 let upper = sql.to_ascii_uppercase();
1060 let trimmed_upper = upper.trim_start();
1061 if !trimmed_upper.starts_with("UPDATE ") {
1062 return sql.to_string();
1063 }
1064 if !upper.contains(" ORDER BY ") && !upper.contains(" LIMIT ") {
1065 return sql.to_string();
1066 }
1067
1068 let kw_pos = find_top_level_keywords(sql, &["SET", "FROM", "WHERE", "ORDER", "LIMIT"]);
1069 let set_pos = match kw_pos.iter().find(|(k, _)| k == "SET") {
1070 Some((_, p)) => *p,
1071 None => return sql.to_string(),
1072 };
1073 if kw_pos.iter().any(|(k, _)| k == "FROM") {
1074 return sql.to_string();
1076 }
1077 let where_pos = kw_pos.iter().find(|(k, _)| k == "WHERE").map(|(_, p)| *p);
1078 let order_pos = kw_pos.iter().find(|(k, _)| k == "ORDER").map(|(_, p)| *p);
1079 let limit_pos = kw_pos.iter().find(|(k, _)| k == "LIMIT").map(|(_, p)| *p);
1080
1081 if order_pos.is_none() && limit_pos.is_none() {
1082 return sql.to_string();
1083 }
1084
1085 let leading_ws = sql.len() - sql.trim_start().len();
1087 let table_section_start = leading_ws + "UPDATE".len();
1088 let table_section = sql[table_section_start..set_pos].trim();
1089 if table_section.is_empty() {
1090 return sql.to_string();
1091 }
1092 let table_for_subquery = table_section
1095 .split_whitespace()
1096 .next()
1097 .unwrap_or(table_section);
1098
1099 let assignments_end = where_pos.or(order_pos).or(limit_pos).unwrap_or(sql.len());
1100 let assignments = sql[set_pos + "SET".len()..assignments_end].trim();
1101 if assignments.is_empty() {
1102 return sql.to_string();
1103 }
1104
1105 let where_body = where_pos.map(|wp| {
1106 let end = order_pos.or(limit_pos).unwrap_or(sql.len());
1107 sql[wp + "WHERE".len()..end].trim()
1108 });
1109
1110 let order_body = order_pos.map(|op| {
1111 let after = sql[op + "ORDER".len()..].trim_start();
1113 let by_skip = sql[op + "ORDER".len()..].len() - after.len();
1114 let body_start = if after.to_ascii_uppercase().starts_with("BY") {
1115 op + "ORDER".len() + by_skip + "BY".len()
1116 } else {
1117 op + "ORDER".len() + by_skip
1118 };
1119 let end = limit_pos.unwrap_or(sql.len());
1120 sql[body_start..end].trim()
1121 });
1122
1123 let limit_body = limit_pos.map(|lp| {
1124 sql[lp + "LIMIT".len()..]
1125 .trim()
1126 .trim_end_matches(';')
1127 .trim()
1128 });
1129
1130 let mut sub = format!("SELECT rowid FROM {table_for_subquery}");
1131 if let Some(w) = where_body {
1132 sub.push_str(" WHERE ");
1133 sub.push_str(w);
1134 }
1135 if let Some(o) = order_body {
1136 sub.push_str(" ORDER BY ");
1137 sub.push_str(o);
1138 }
1139 if let Some(l) = limit_body {
1140 sub.push_str(" LIMIT ");
1141 sub.push_str(l);
1142 }
1143
1144 format!("UPDATE {table_section} SET {assignments} WHERE rowid IN ({sub})")
1145}
1146
1147fn find_top_level_keywords(sql: &str, keywords: &[&str]) -> Vec<(String, usize)> {
1152 let bytes = sql.as_bytes();
1153 let n = bytes.len();
1154 let mut out: Vec<(String, usize)> = Vec::new();
1155 let mut i = 0usize;
1156 let mut depth = 0i32;
1157
1158 while i < n {
1159 let ch = bytes[i] as char;
1160 if ch == '\'' || ch == '"' {
1162 let quote = ch;
1163 i += 1;
1164 while i < n {
1165 let c = bytes[i] as char;
1166 i += 1;
1167 if c == quote {
1168 if i < n && (bytes[i] as char) == quote {
1169 i += 1;
1170 continue;
1171 }
1172 break;
1173 }
1174 }
1175 continue;
1176 }
1177 if ch == '-' && i + 1 < n && bytes[i + 1] as char == '-' {
1179 while i < n && bytes[i] as char != '\n' {
1180 i += 1;
1181 }
1182 continue;
1183 }
1184 if ch == '(' {
1185 depth += 1;
1186 i += 1;
1187 continue;
1188 }
1189 if ch == ')' {
1190 depth -= 1;
1191 i += 1;
1192 continue;
1193 }
1194 if depth != 0 {
1195 i += 1;
1196 continue;
1197 }
1198
1199 let prev_is_word = i > 0 && {
1200 let p = bytes[i - 1] as char;
1201 p.is_ascii_alphanumeric() || p == '_'
1202 };
1203 if !prev_is_word {
1204 for kw in keywords {
1205 let kb = kw.as_bytes();
1206 if i + kb.len() > n {
1207 continue;
1208 }
1209 let mut ok = true;
1210 for (k, b) in kb.iter().enumerate() {
1211 if bytes[i + k].to_ascii_uppercase() != *b {
1212 ok = false;
1213 break;
1214 }
1215 }
1216 if !ok {
1217 continue;
1218 }
1219 if i + kb.len() < n {
1220 let nc = bytes[i + kb.len()] as char;
1221 if nc.is_ascii_alphanumeric() || nc == '_' {
1222 continue;
1223 }
1224 }
1225 out.push((kw.to_string(), i));
1226 break;
1227 }
1228 }
1229 i += 1;
1230 }
1231 out
1232}
1233
1234fn preprocess_pragma(sql: &str) -> String {
1235 let trimmed = sql.trim();
1236 if !trimmed.to_uppercase().starts_with("PRAGMA ") {
1237 return sql.to_string();
1238 }
1239 let after_pragma = trimmed[7..].trim();
1240 if let Some(paren_start) = after_pragma.find('(') {
1241 if let Some(paren_end) = after_pragma.rfind(')') {
1242 let arg = after_pragma[paren_start + 1..paren_end].trim();
1243 if !arg.starts_with('\'') && !arg.starts_with('"') {
1244 let name = &after_pragma[..paren_start];
1245 let rest = if paren_end + 1 < after_pragma.len() {
1246 &after_pragma[paren_end + 1..]
1247 } else {
1248 ""
1249 };
1250 return format!("PRAGMA {name}('{arg}'){rest}");
1251 }
1252 }
1253 }
1254 if let Some(eq_pos) = after_pragma.find('=') {
1255 let name = after_pragma[..eq_pos].trim();
1256 let val = after_pragma[eq_pos + 1..].trim().trim_end_matches(';');
1257 let val = val.trim();
1258 if val.eq_ignore_ascii_case("ON")
1259 || val.eq_ignore_ascii_case("YES")
1260 || val.eq_ignore_ascii_case("TRUE")
1261 {
1262 return format!("PRAGMA {name} = 1;");
1263 }
1264 if val.eq_ignore_ascii_case("OFF")
1265 || val.eq_ignore_ascii_case("NO")
1266 || val.eq_ignore_ascii_case("FALSE")
1267 {
1268 return format!("PRAGMA {name} = 0;");
1269 }
1270 if !val.starts_with('\'') && !val.starts_with('"') && val.parse::<i64>().is_err() {
1271 return format!("PRAGMA {name} = '{val}';");
1272 }
1273 }
1274 sql.to_string()
1275}
1276
1277fn preprocess_fts5_match(sql: &str) -> String {
1287 if !contains_keyword_ci(sql, "MATCH") {
1288 return sql.to_string();
1289 }
1290 let bytes = sql.as_bytes();
1291 let n = bytes.len();
1292 let mut out = String::with_capacity(n);
1293 let mut i = 0usize;
1294
1295 while i < n {
1296 let ch = bytes[i] as char;
1297 if ch == '\'' || ch == '"' {
1298 let quote = ch;
1299 out.push(ch);
1300 i += 1;
1301 while i < n {
1302 let c = bytes[i] as char;
1303 out.push(c);
1304 i += 1;
1305 if c == quote {
1306 if i < n && (bytes[i] as char) == quote {
1307 out.push(quote);
1308 i += 1;
1309 continue;
1310 }
1311 break;
1312 }
1313 }
1314 continue;
1315 }
1316 if ch == '-' && i + 1 < n && (bytes[i + 1] as char) == '-' {
1317 while i < n && (bytes[i] as char) != '\n' {
1318 out.push(bytes[i] as char);
1319 i += 1;
1320 }
1321 continue;
1322 }
1323
1324 if (ch == 'M' || ch == 'm') && looks_like_match_kw(bytes, i) {
1325 if let Some((id_start, id)) = trailing_identifier(&out) {
1326 let mut j = i + 5;
1327 let saw_ws = j < n && (bytes[j] as char).is_whitespace();
1328 while j < n && (bytes[j] as char).is_whitespace() {
1329 j += 1;
1330 }
1331 if saw_ws && j < n && (bytes[j] as char) == '\'' {
1332 let lit_start = j;
1333 j += 1;
1334 while j < n {
1335 let c = bytes[j] as char;
1336 if c == '\'' {
1337 if j + 1 < n && (bytes[j + 1] as char) == '\'' {
1338 j += 2;
1339 continue;
1340 }
1341 j += 1;
1342 break;
1343 }
1344 j += 1;
1345 }
1346 let lit = &sql[lit_start..j];
1347 let lhs_quoted = id.replace('\'', "''");
1348 out.truncate(id_start);
1349 out.push_str(&format!("__fts5_match_token('{lhs_quoted}', {lit})"));
1350 i = j;
1351 continue;
1352 }
1353 }
1354 }
1355
1356 out.push(ch);
1357 i += 1;
1358 }
1359 out
1360}
1361
1362fn looks_like_match_kw(bytes: &[u8], i: usize) -> bool {
1363 if i + 5 > bytes.len() {
1364 return false;
1365 }
1366 let head = &bytes[i..i + 5];
1367 if !(head[0].eq_ignore_ascii_case(&b'M')
1368 && head[1].eq_ignore_ascii_case(&b'A')
1369 && head[2].eq_ignore_ascii_case(&b'T')
1370 && head[3].eq_ignore_ascii_case(&b'C')
1371 && head[4].eq_ignore_ascii_case(&b'H'))
1372 {
1373 return false;
1374 }
1375 if i > 0 {
1376 let prev = bytes[i - 1] as char;
1377 if prev.is_ascii_alphanumeric() || prev == '_' {
1378 return false;
1379 }
1380 }
1381 if let Some(b) = bytes.get(i + 5) {
1382 let c = *b as char;
1383 if c.is_ascii_alphanumeric() || c == '_' {
1384 return false;
1385 }
1386 }
1387 true
1388}
1389
1390fn trailing_identifier(out: &str) -> Option<(usize, String)> {
1391 let trimmed = out.trim_end();
1392 let bytes = trimmed.as_bytes();
1393 let n = bytes.len();
1394 if n == 0 {
1395 return None;
1396 }
1397 if bytes[n - 1] == b'"' {
1398 let k = n - 1;
1399 if k == 0 {
1400 return None;
1401 }
1402 let mut start = None;
1403 let mut j = k;
1404 while j > 0 {
1405 j -= 1;
1406 if bytes[j] == b'"' {
1407 start = Some(j);
1408 break;
1409 }
1410 }
1411 let s = start?;
1412 let mut q_start = s;
1413 if q_start > 0 && bytes[q_start - 1] == b'.' {
1414 let mut p = q_start - 1;
1415 while p > 0 {
1416 let c = bytes[p - 1] as char;
1417 if c.is_ascii_alphanumeric() || c == '_' {
1418 p -= 1;
1419 } else {
1420 break;
1421 }
1422 }
1423 q_start = p;
1424 }
1425 let text = String::from_utf8_lossy(&bytes[q_start..n]).into_owned();
1426 return Some((q_start, text));
1427 }
1428 let last = bytes[n - 1] as char;
1429 if !(last.is_ascii_alphanumeric() || last == '_') {
1430 return None;
1431 }
1432 let mut start = n;
1433 while start > 0 {
1434 let c = bytes[start - 1] as char;
1435 if c.is_ascii_alphanumeric() || c == '_' || c == '.' {
1436 start -= 1;
1437 } else {
1438 break;
1439 }
1440 }
1441 let text = String::from_utf8_lossy(&bytes[start..n]).into_owned();
1442 Some((start, text))
1443}
1444
1445fn contains_keyword_ci(sql: &str, kw: &str) -> bool {
1446 let lower = sql.to_ascii_lowercase();
1447 let needle = kw.to_ascii_lowercase();
1448 let mut idx = 0;
1449 while let Some(p) = lower[idx..].find(&needle) {
1450 let abs = idx + p;
1451 let before_ok = abs == 0
1452 || !lower.as_bytes()[abs - 1].is_ascii_alphanumeric()
1453 && lower.as_bytes()[abs - 1] != b'_';
1454 let after = abs + needle.len();
1455 let after_ok = after == lower.len()
1456 || (!lower.as_bytes()[after].is_ascii_alphanumeric()
1457 && lower.as_bytes()[after] != b'_');
1458 if before_ok && after_ok {
1459 return true;
1460 }
1461 idx = abs + needle.len();
1462 if idx >= lower.len() {
1463 break;
1464 }
1465 }
1466 false
1467}
1468
1469#[cfg(test)]
1470mod tests {
1471 use super::*;
1472
1473 #[test]
1474 fn parse_simple_select() {
1475 let stmts = parse_sql("SELECT * FROM users").unwrap();
1476 assert_eq!(stmts.len(), 1);
1477 }
1478
1479 #[test]
1480 fn parse_create_table() {
1481 let stmts =
1482 parse_sql("CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)").unwrap();
1483 assert_eq!(stmts.len(), 1);
1484 }
1485
1486 #[test]
1487 fn parse_insert() {
1488 let stmts = parse_sql("INSERT INTO users (name) VALUES ('alice')").unwrap();
1489 assert_eq!(stmts.len(), 1);
1490 }
1491
1492 #[test]
1493 fn parse_error() {
1494 assert!(parse_sql("SELECTT * FROM").is_err());
1495 }
1496
1497 fn assert_pseudo_pragma(sql: &str, expected_name: &str) {
1500 let stmts = parse_sql(sql).unwrap();
1501 assert_eq!(stmts.len(), 1, "expected one statement for {sql:?}");
1502 match &stmts[0] {
1503 sqlparser::ast::Statement::Pragma { name, .. } => {
1504 assert_eq!(
1505 name.to_string().to_lowercase(),
1506 expected_name,
1507 "wrong pseudo-pragma for {sql:?}"
1508 );
1509 }
1510 other => panic!("expected pragma statement for {sql:?}, got {other:?}"),
1511 }
1512 }
1513
1514 #[test]
1517 fn parse_vacuum_bare() {
1518 assert_pseudo_pragma("VACUUM", "__vacuum");
1519 assert_pseudo_pragma("vacuum;", "__vacuum");
1520 }
1521
1522 #[test]
1523 fn parse_reindex_no_arg() {
1524 assert_pseudo_pragma("REINDEX", "__reindex");
1525 }
1526
1527 #[test]
1528 fn parse_reindex_with_target() {
1529 assert_pseudo_pragma("REINDEX my_index", "__reindex");
1530 assert_pseudo_pragma("reindex MY_TABLE;", "__reindex");
1531 }
1532
1533 #[test]
1534 fn parse_analyze_no_arg() {
1535 assert_pseudo_pragma("ANALYZE", "__analyze");
1536 assert_pseudo_pragma("analyze ;", "__analyze");
1537 }
1538
1539 #[test]
1540 fn parse_analyze_with_target() {
1541 assert_pseudo_pragma("ANALYZE users", "__analyze");
1542 }
1543
1544 #[test]
1547 fn parse_detach_database() {
1548 assert_pseudo_pragma("DETACH DATABASE aux", "__detach");
1549 assert_pseudo_pragma("DETACH aux;", "__detach");
1550 }
1551
1552 #[test]
1555 fn parse_create_trigger_after_insert() {
1556 assert_pseudo_pragma(
1557 "CREATE TRIGGER tlog AFTER INSERT ON t BEGIN SELECT 1; END",
1558 "__create_trigger",
1559 );
1560 }
1561
1562 #[test]
1563 fn parse_create_trigger_before_update_for_each_row() {
1564 assert_pseudo_pragma(
1565 "CREATE TRIGGER t1 BEFORE UPDATE ON things FOR EACH ROW BEGIN SELECT 1; END",
1566 "__create_trigger",
1567 );
1568 }
1569
1570 #[test]
1571 fn parse_create_trigger_if_not_exists() {
1572 assert_pseudo_pragma(
1573 "CREATE TRIGGER IF NOT EXISTS t1 AFTER DELETE ON x BEGIN SELECT 1; END",
1574 "__create_trigger",
1575 );
1576 }
1577
1578 #[test]
1579 fn parse_drop_trigger() {
1580 assert_pseudo_pragma("DROP TRIGGER tlog", "__drop_trigger");
1581 assert_pseudo_pragma("DROP TRIGGER IF EXISTS tlog", "__drop_trigger");
1582 }
1583
1584 #[test]
1587 fn parse_pragma_paren_form() {
1588 let stmts = parse_sql("PRAGMA table_info(users)").unwrap();
1591 assert_eq!(stmts.len(), 1);
1592 assert!(matches!(
1593 &stmts[0],
1594 sqlparser::ast::Statement::Pragma { .. }
1595 ));
1596 }
1597
1598 #[test]
1599 fn parse_pragma_equals_form() {
1600 let stmts = parse_sql("PRAGMA foreign_keys = ON").unwrap();
1601 assert_eq!(stmts.len(), 1);
1602 assert!(matches!(
1603 &stmts[0],
1604 sqlparser::ast::Statement::Pragma { .. }
1605 ));
1606 }
1607
1608 #[test]
1609 fn parse_pragma_simple_read_form() {
1610 let stmts = parse_sql("PRAGMA page_size").unwrap();
1611 assert_eq!(stmts.len(), 1);
1612 assert!(matches!(
1613 &stmts[0],
1614 sqlparser::ast::Statement::Pragma { .. }
1615 ));
1616 }
1617
1618 #[test]
1621 fn parse_multiple_statements() {
1622 let stmts = parse_sql("SELECT 1; SELECT 2;").unwrap();
1623 assert_eq!(stmts.len(), 2);
1624 }
1625}
1626
1627#[cfg(test)]
1628mod update_limit_tests {
1629 use super::*;
1630
1631 #[test]
1632 fn rewrite_limit_only() {
1633 let out = preprocess_update_limit("UPDATE t SET status = 'new' LIMIT 2");
1634 assert!(out.contains("LIMIT 2"), "out={out}");
1635 assert!(out.contains("rowid IN ("), "out={out}");
1636 }
1637
1638 #[test]
1639 fn rewrite_where_and_limit() {
1640 let out =
1641 preprocess_update_limit("UPDATE t SET status = 'updated' WHERE status = 'a' LIMIT 2");
1642 assert!(out.contains("WHERE status = 'a'"), "out={out}");
1643 assert!(out.contains("LIMIT 2"));
1644 assert!(out.contains("rowid IN ("));
1645 }
1646
1647 #[test]
1648 fn no_op_without_order_by_or_limit() {
1649 let sql = "UPDATE t SET status = 'x' WHERE id = 1";
1650 let out = preprocess_update_limit(sql);
1651 assert_eq!(out, sql);
1652 }
1653
1654 #[test]
1655 fn skips_string_literal_limit() {
1656 let sql = "UPDATE t SET label = 'has LIMIT in it' WHERE id = 1";
1657 let out = preprocess_update_limit(sql);
1658 assert_eq!(out, sql);
1659 }
1660}