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