1#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct Statement {
9 pub text: String,
11 pub line: usize,
13}
14
15pub fn split(sql: &str) -> Vec<Statement> {
16 let b = sql.as_bytes();
17 let mut out = Vec::new();
18 let mut start = 0;
19 let mut i = 0;
20 while i < b.len() {
21 match b[i] {
22 b'\'' | b'"' | b'`' => i = skip_quoted(b, i),
23 b'-' if b.get(i + 1) == Some(&b'-') => {
24 while i < b.len() && b[i] != b'\n' {
25 i += 1;
26 }
27 }
28 b'/' if b.get(i + 1) == Some(&b'*') => i = skip_block_comment(b, i),
29 b'$' => i = skip_dollar_quoted(sql, i),
30 b';' => {
31 push_statement(sql, start, i, &mut out);
32 i += 1;
33 start = i;
34 }
35 _ => i += 1,
36 }
37 }
38 push_statement(sql, start, b.len(), &mut out);
39 out
40}
41
42fn push_statement(sql: &str, start: usize, end: usize, out: &mut Vec<Statement>) {
43 let raw = &sql[start..end];
44 let text = raw.trim();
45 if text.is_empty() || strip_leading_comments(text).is_empty() {
46 return;
47 }
48 let lead = raw.len() - raw.trim_start().len();
49 let line = sql[..start + lead].matches('\n').count() + 1;
50 out.push(Statement {
51 text: text.to_string(),
52 line,
53 });
54}
55
56fn skip_quoted(b: &[u8], i: usize) -> usize {
57 let q = b[i];
58 let mut j = i + 1;
59 while j < b.len() {
60 if b[j] == q {
61 if b.get(j + 1) == Some(&q) {
63 j += 2;
64 continue;
65 }
66 return j + 1;
67 }
68 j += 1;
69 }
70 b.len()
71}
72
73fn skip_block_comment(b: &[u8], i: usize) -> usize {
74 let mut depth = 0;
75 let mut j = i;
76 while j < b.len() {
77 if b[j] == b'/' && b.get(j + 1) == Some(&b'*') {
78 depth += 1;
79 j += 2;
80 } else if b[j] == b'*' && b.get(j + 1) == Some(&b'/') {
81 depth -= 1;
82 j += 2;
83 if depth == 0 {
84 return j;
85 }
86 } else {
87 j += 1;
88 }
89 }
90 b.len()
91}
92
93fn skip_dollar_quoted(sql: &str, i: usize) -> usize {
94 let b = sql.as_bytes();
95 let mut j = i + 1;
97 if j < b.len() && (b[j].is_ascii_alphabetic() || b[j] == b'_') {
98 while j < b.len() && (b[j].is_ascii_alphanumeric() || b[j] == b'_') {
99 j += 1;
100 }
101 }
102 if b.get(j) != Some(&b'$') {
103 return i + 1;
104 }
105 let tag = &sql[i..=j];
106 match sql[j + 1..].find(tag) {
107 Some(pos) => j + 1 + pos + tag.len(),
108 None => b.len(),
109 }
110}
111
112pub fn strip_leading_comments(mut s: &str) -> &str {
114 loop {
115 s = s.trim_start();
116 if let Some(rest) = s.strip_prefix("--") {
117 s = rest.find('\n').map_or("", |p| &rest[p..]);
118 } else if s.starts_with("/*") {
119 let end = skip_block_comment(s.as_bytes(), 0);
120 s = &s[end..];
121 } else {
122 return s;
123 }
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
128pub enum StatementKind {
129 Read,
131 TempCreate,
133 Other,
135}
136
137impl StatementKind {
138 pub fn is_read_only_safe(self) -> bool {
140 matches!(self, StatementKind::Read | StatementKind::TempCreate)
141 }
142}
143
144pub fn classify(statement: &str) -> StatementKind {
147 let s = strip_leading_comments(statement).trim_start_matches(|c: char| c == '(' || c.is_whitespace());
148 let words: Vec<String> = s
149 .split_whitespace()
150 .take(5)
151 .map(|w| w.trim_matches('(').to_ascii_lowercase())
152 .collect();
153 let w = |n: usize| words.get(n).map(String::as_str).unwrap_or("");
154 match w(0) {
155 "select" | "with" => StatementKind::Read,
156 "create" => {
157 let mut k = 1;
158 if w(1) == "or" && w(2) == "replace" {
159 k = 3;
160 }
161 if matches!(w(k), "temp" | "temporary") && matches!(w(k + 1), "table" | "view") {
162 StatementKind::TempCreate
163 } else {
164 StatementKind::Other
165 }
166 }
167 _ => StatementKind::Other,
168 }
169}