sql_cli/sql/
script_parser.rs

1// Script parser for handling multi-statement SQL scripts with GO separator
2// Similar to SQL Server's batch execution model
3
4use anyhow::Result;
5
6/// Parses SQL scripts into individual statements using GO as separator
7pub struct ScriptParser {
8    content: String,
9}
10
11impl ScriptParser {
12    /// Create a new script parser with the given content
13    pub fn new(content: &str) -> Self {
14        Self {
15            content: content.to_string(),
16        }
17    }
18
19    /// Parse the script into individual SQL statements
20    /// GO must be on its own line (case-insensitive)
21    /// Returns a vector of SQL statements to execute
22    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            // Check if this line is just "GO" (case-insensitive)
30            if trimmed.eq_ignore_ascii_case("go") {
31                // Add the current statement if it's not empty or just comments
32                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                // Add this line to the current statement
39                if !current_statement.is_empty() {
40                    current_statement.push('\n');
41                }
42                current_statement.push_str(line);
43            }
44        }
45
46        // Don't forget the last statement if there's no trailing GO
47        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    /// Check if a statement contains only comments (no actual SQL)
56    fn is_comment_only(statement: &str) -> bool {
57        for line in statement.lines() {
58            let trimmed = line.trim();
59            // Skip empty lines and comments
60            if trimmed.is_empty() || trimmed.starts_with("--") {
61                continue;
62            }
63            // If we find any non-comment content, it's not comment-only
64            return false;
65        }
66        // All lines were comments or empty
67        true
68    }
69
70    /// Parse and validate that all statements are valid SQL
71    /// Returns the statements or an error if any are invalid
72    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        // Basic validation - ensure no statement is just whitespace
80        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/// Result of executing a single statement in a script
91#[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/// Result of executing an entire script
102#[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}