sql_cli/sql/
script_parser.rs1use anyhow::Result;
5
6pub struct ScriptParser {
8 content: String,
9}
10
11impl ScriptParser {
12 pub fn new(content: &str) -> Self {
14 Self {
15 content: content.to_string(),
16 }
17 }
18
19 pub fn parse_statements(&self) -> Vec<String> {
23 let mut statements = Vec::new();
24 let mut current_statement = String::new();
25
26 for line in self.content.lines() {
27 let trimmed = line.trim();
28
29 if trimmed.eq_ignore_ascii_case("go") {
31 let statement = current_statement.trim().to_string();
33 if !statement.is_empty() && !Self::is_comment_only(&statement) {
34 statements.push(statement);
35 }
36 current_statement.clear();
37 } else {
38 if !current_statement.is_empty() {
40 current_statement.push('\n');
41 }
42 current_statement.push_str(line);
43 }
44 }
45
46 let statement = current_statement.trim().to_string();
48 if !statement.is_empty() && !Self::is_comment_only(&statement) {
49 statements.push(statement);
50 }
51
52 statements
53 }
54
55 fn is_comment_only(statement: &str) -> bool {
57 for line in statement.lines() {
58 let trimmed = line.trim();
59 if trimmed.is_empty() || trimmed.starts_with("--") {
61 continue;
62 }
63 return false;
65 }
66 true
68 }
69
70 pub fn parse_and_validate(&self) -> Result<Vec<String>> {
73 let statements = self.parse_statements();
74
75 if statements.is_empty() {
76 anyhow::bail!("No SQL statements found in script");
77 }
78
79 for (i, stmt) in statements.iter().enumerate() {
81 if stmt.trim().is_empty() {
82 anyhow::bail!("Empty statement at position {}", i + 1);
83 }
84 }
85
86 Ok(statements)
87 }
88}
89
90#[derive(Debug)]
92pub struct StatementResult {
93 pub statement_number: usize,
94 pub sql: String,
95 pub success: bool,
96 pub rows_affected: usize,
97 pub error_message: Option<String>,
98 pub execution_time_ms: f64,
99}
100
101#[derive(Debug)]
103pub struct ScriptResult {
104 pub total_statements: usize,
105 pub successful_statements: usize,
106 pub failed_statements: usize,
107 pub total_execution_time_ms: f64,
108 pub statement_results: Vec<StatementResult>,
109}
110
111impl ScriptResult {
112 pub fn new() -> Self {
113 Self {
114 total_statements: 0,
115 successful_statements: 0,
116 failed_statements: 0,
117 total_execution_time_ms: 0.0,
118 statement_results: Vec::new(),
119 }
120 }
121
122 pub fn add_success(&mut self, statement_number: usize, sql: String, rows: usize, time_ms: f64) {
123 self.total_statements += 1;
124 self.successful_statements += 1;
125 self.total_execution_time_ms += time_ms;
126
127 self.statement_results.push(StatementResult {
128 statement_number,
129 sql,
130 success: true,
131 rows_affected: rows,
132 error_message: None,
133 execution_time_ms: time_ms,
134 });
135 }
136
137 pub fn add_failure(
138 &mut self,
139 statement_number: usize,
140 sql: String,
141 error: String,
142 time_ms: f64,
143 ) {
144 self.total_statements += 1;
145 self.failed_statements += 1;
146 self.total_execution_time_ms += time_ms;
147
148 self.statement_results.push(StatementResult {
149 statement_number,
150 sql,
151 success: false,
152 rows_affected: 0,
153 error_message: Some(error),
154 execution_time_ms: time_ms,
155 });
156 }
157
158 pub fn all_successful(&self) -> bool {
159 self.failed_statements == 0
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn test_parse_single_statement() {
169 let script = "SELECT * FROM users";
170 let parser = ScriptParser::new(script);
171 let statements = parser.parse_statements();
172
173 assert_eq!(statements.len(), 1);
174 assert_eq!(statements[0], "SELECT * FROM users");
175 }
176
177 #[test]
178 fn test_parse_multiple_statements_with_go() {
179 let script = r"
180SELECT * FROM users
181GO
182SELECT * FROM orders
183GO
184SELECT * FROM products
185";
186 let parser = ScriptParser::new(script);
187 let statements = parser.parse_statements();
188
189 assert_eq!(statements.len(), 3);
190 assert_eq!(statements[0].trim(), "SELECT * FROM users");
191 assert_eq!(statements[1].trim(), "SELECT * FROM orders");
192 assert_eq!(statements[2].trim(), "SELECT * FROM products");
193 }
194
195 #[test]
196 fn test_go_case_insensitive() {
197 let script = r"
198SELECT 1
199go
200SELECT 2
201Go
202SELECT 3
203GO
204";
205 let parser = ScriptParser::new(script);
206 let statements = parser.parse_statements();
207
208 assert_eq!(statements.len(), 3);
209 }
210
211 #[test]
212 fn test_go_in_string_not_separator() {
213 let script = r"
214SELECT 'This string contains GO but should not split' as test
215GO
216SELECT 'Another statement' as test2
217";
218 let parser = ScriptParser::new(script);
219 let statements = parser.parse_statements();
220
221 assert_eq!(statements.len(), 2);
222 assert!(statements[0].contains("GO but should not split"));
223 }
224
225 #[test]
226 fn test_multiline_statements() {
227 let script = r"
228SELECT
229 id,
230 name,
231 email
232FROM users
233WHERE active = true
234GO
235SELECT COUNT(*)
236FROM orders
237";
238 let parser = ScriptParser::new(script);
239 let statements = parser.parse_statements();
240
241 assert_eq!(statements.len(), 2);
242 assert!(statements[0].contains("WHERE active = true"));
243 }
244
245 #[test]
246 fn test_empty_statements_filtered() {
247 let script = r"
248GO
249SELECT 1
250GO
251GO
252SELECT 2
253GO
254";
255 let parser = ScriptParser::new(script);
256 let statements = parser.parse_statements();
257
258 assert_eq!(statements.len(), 2);
259 assert_eq!(statements[0].trim(), "SELECT 1");
260 assert_eq!(statements[1].trim(), "SELECT 2");
261 }
262}