cqlite_cli/
script_executor.rs1use anyhow::{Context, Result};
8use cqlite_core::Database;
9use std::path::Path;
10
11use crate::cli::OutputFormat;
12use crate::config::OutputConfig;
13use crate::error::{print_error, CliExitCode};
14
15#[cfg(feature = "state_machine")]
61pub async fn execute_script_file(
62 file_path: &Path,
63 database: &Database,
64 output_config: &OutputConfig,
65 format: OutputFormat,
66) -> Result<()> {
67 let statements = load_script(file_path)
69 .with_context(|| format!("Failed to parse script file: {}", file_path.display()))?;
70
71 if statements.is_empty() {
72 eprintln!("Warning: Script file contains no statements");
73 return Ok(());
74 }
75
76 println!(
77 "Executing {} statement(s) from {}",
78 statements.len(),
79 file_path.display()
80 );
81
82 for (index, statement) in statements.iter().enumerate() {
84 let statement_num = index + 1;
85
86 if let Err(e) = crate::commands::execute_query(
88 database,
89 statement,
90 false, false, format.clone(),
93 output_config,
94 )
95 .await
96 {
97 eprintln!(
99 "Error executing statement {} in {}",
100 statement_num,
101 file_path.display()
102 );
103 eprintln!("Statement: {}", statement);
104 print_error(&e, CliExitCode::QueryExecutionError);
105
106 std::process::exit(CliExitCode::QueryExecutionError.as_i32());
108 }
109 }
110
111 println!("\nSuccessfully executed {} statement(s)", statements.len());
112 Ok(())
113}
114
115#[cfg(not(feature = "state_machine"))]
117pub async fn execute_script_file(
118 _file_path: &Path,
119 _database: &Database,
120 _output_config: &OutputConfig,
121 _format: OutputFormat,
122) -> Result<()> {
123 anyhow::bail!(
124 "Script execution is not available in M1.\n\
125 Build with --features state_machine to enable this feature.\n\
126 See CLAUDE.md for M1 API examples."
127 )
128}
129
130pub fn parse_script(script_content: &str) -> Result<Vec<String>> {
151 let mut statements = Vec::new();
152 let mut current_statement = String::new();
153 let mut chars = script_content.chars().peekable();
154
155 let mut in_string = false;
156 let mut in_line_comment = false;
157 let mut in_block_comment = false;
158 let mut string_delimiter = '\0';
159
160 while let Some(ch) = chars.next() {
161 if !in_string && !in_block_comment && ch == '-' {
163 if chars.peek() == Some(&'-') {
164 chars.next(); in_line_comment = true;
166 continue;
167 }
168 }
169
170 if in_line_comment {
172 if ch == '\n' {
173 in_line_comment = false;
174 current_statement.push(ch);
175 }
176 continue;
177 }
178
179 if !in_string && !in_line_comment && ch == '/' {
181 if chars.peek() == Some(&'*') {
182 chars.next(); in_block_comment = true;
184 continue;
185 }
186 }
187
188 if in_block_comment {
190 if ch == '*' && chars.peek() == Some(&'/') {
191 chars.next(); in_block_comment = false;
193 }
194 continue;
195 }
196
197 if !in_block_comment && !in_line_comment {
199 if ch == '\'' || ch == '"' {
200 if !in_string {
201 in_string = true;
202 string_delimiter = ch;
203 current_statement.push(ch);
204 } else if ch == string_delimiter {
205 if chars.peek() == Some(&ch) {
207 current_statement.push(ch);
208 current_statement.push(chars.next().unwrap());
209 } else {
210 in_string = false;
211 current_statement.push(ch);
212 }
213 } else {
214 current_statement.push(ch);
215 }
216 continue;
217 }
218 }
219
220 if !in_string && !in_line_comment && !in_block_comment && ch == ';' {
222 let trimmed = current_statement.trim();
223 if !trimmed.is_empty() {
224 statements.push(trimmed.to_string());
225 }
226 current_statement.clear();
227 continue;
228 }
229
230 if !in_line_comment && !in_block_comment {
232 current_statement.push(ch);
233 }
234 }
235
236 if in_string {
238 anyhow::bail!("Unterminated string literal in script");
239 }
240
241 if in_block_comment {
242 anyhow::bail!("Unterminated block comment in script");
243 }
244
245 let remaining = current_statement.trim();
247 if !remaining.is_empty() {
248 anyhow::bail!(
249 "Unterminated statement found (missing semicolon): {}",
250 if remaining.len() > 50 {
251 format!("{}...", &remaining[..50])
252 } else {
253 remaining.to_string()
254 }
255 );
256 }
257
258 Ok(statements)
259}
260
261pub fn load_script(script_path: &Path) -> Result<Vec<String>> {
263 let content = std::fs::read_to_string(script_path)
264 .with_context(|| format!("Failed to read script file: {}", script_path.display()))?;
265
266 parse_script(&content)
267 .with_context(|| format!("Failed to parse script file: {}", script_path.display()))
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn test_parse_empty_file() {
276 let result = parse_script("");
277 assert!(result.is_ok());
278 assert_eq!(result.unwrap().len(), 0);
279 }
280
281 #[test]
282 fn test_parse_single_statement() {
283 let content = "SELECT * FROM users;";
284 let result = parse_script(content);
285 assert!(result.is_ok());
286 let statements = result.unwrap();
287 assert_eq!(statements.len(), 1);
288 assert_eq!(statements[0], "SELECT * FROM users");
289 }
290
291 #[test]
292 fn test_parse_multiple_statements() {
293 let content = "SELECT * FROM users;\nINSERT INTO users VALUES (1, 'test');";
294 let result = parse_script(content);
295 assert!(result.is_ok());
296 let statements = result.unwrap();
297 assert_eq!(statements.len(), 2);
298 assert_eq!(statements[0], "SELECT * FROM users");
299 assert_eq!(statements[1], "INSERT INTO users VALUES (1, 'test')");
300 }
301
302 #[test]
303 fn test_parse_line_comments() {
304 let content = "-- This is a comment\nSELECT * FROM users; -- inline comment";
305 let result = parse_script(content);
306 assert!(result.is_ok());
307 let statements = result.unwrap();
308 assert_eq!(statements.len(), 1);
309 assert_eq!(statements[0], "SELECT * FROM users");
310 }
311
312 #[test]
313 fn test_parse_block_comments() {
314 let content = "/* block comment */ SELECT * FROM users; /* another comment */";
315 let result = parse_script(content);
316 assert!(result.is_ok());
317 let statements = result.unwrap();
318 assert_eq!(statements.len(), 1);
319 assert_eq!(statements[0], "SELECT * FROM users");
320 }
321
322 #[test]
323 fn test_parse_multiline_block_comment() {
324 let content = "/*\n * Multi-line\n * block comment\n */\nSELECT * FROM users;";
325 let result = parse_script(content);
326 assert!(result.is_ok());
327 let statements = result.unwrap();
328 assert_eq!(statements.len(), 1);
329 assert_eq!(statements[0], "SELECT * FROM users");
330 }
331
332 #[test]
333 fn test_parse_string_with_semicolon() {
334 let content = "INSERT INTO users VALUES (1, 'test;value');";
335 let result = parse_script(content);
336 assert!(result.is_ok());
337 let statements = result.unwrap();
338 assert_eq!(statements.len(), 1);
339 assert_eq!(statements[0], "INSERT INTO users VALUES (1, 'test;value')");
340 }
341
342 #[test]
343 fn test_parse_double_quoted_string() {
344 let content = r#"INSERT INTO users VALUES (1, "test;value");"#;
345 let result = parse_script(content);
346 assert!(result.is_ok());
347 let statements = result.unwrap();
348 assert_eq!(statements.len(), 1);
349 assert_eq!(
350 statements[0],
351 r#"INSERT INTO users VALUES (1, "test;value")"#
352 );
353 }
354
355 #[test]
356 fn test_parse_escaped_single_quotes() {
357 let content = "INSERT INTO users VALUES (1, 'test''s value');";
358 let result = parse_script(content);
359 assert!(result.is_ok());
360 let statements = result.unwrap();
361 assert_eq!(statements.len(), 1);
362 assert_eq!(
363 statements[0],
364 "INSERT INTO users VALUES (1, 'test''s value')"
365 );
366 }
367
368 #[test]
369 fn test_parse_escaped_double_quotes() {
370 let content = r#"INSERT INTO users VALUES (1, "test""s value");"#;
371 let result = parse_script(content);
372 assert!(result.is_ok());
373 let statements = result.unwrap();
374 assert_eq!(statements.len(), 1);
375 assert_eq!(
376 statements[0],
377 r#"INSERT INTO users VALUES (1, "test""s value")"#
378 );
379 }
380
381 #[test]
382 fn test_parse_multiline_statement() {
383 let content = "SELECT *\nFROM users\nWHERE id = 1;";
384 let result = parse_script(content);
385 assert!(result.is_ok());
386 let statements = result.unwrap();
387 assert_eq!(statements.len(), 1);
388 assert_eq!(statements[0], "SELECT *\nFROM users\nWHERE id = 1");
389 }
390
391 #[test]
392 fn test_parse_blank_lines() {
393 let content = "SELECT * FROM users;\n\n\nINSERT INTO users VALUES (1, 'test');";
394 let result = parse_script(content);
395 assert!(result.is_ok());
396 let statements = result.unwrap();
397 assert_eq!(statements.len(), 2);
398 }
399
400 #[test]
401 fn test_parse_comments_only() {
402 let content = "-- comment 1\n/* comment 2 */\n-- comment 3";
403 let result = parse_script(content);
404 assert!(result.is_ok());
405 assert_eq!(result.unwrap().len(), 0);
406 }
407
408 #[test]
409 fn test_parse_unterminated_statement() {
410 let content = "SELECT * FROM users";
411 let result = parse_script(content);
412 assert!(result.is_err());
413 assert!(result
414 .unwrap_err()
415 .to_string()
416 .contains("Unterminated statement"));
417 }
418
419 #[test]
420 fn test_parse_unterminated_string() {
421 let content = "INSERT INTO users VALUES (1, 'unterminated;";
422 let result = parse_script(content);
423 assert!(result.is_err());
424 assert!(result
425 .unwrap_err()
426 .to_string()
427 .contains("Unterminated string"));
428 }
429
430 #[test]
431 fn test_parse_unterminated_block_comment() {
432 let content = "/* unterminated comment\nSELECT * FROM users;";
433 let result = parse_script(content);
434 assert!(result.is_err());
435 assert!(result
436 .unwrap_err()
437 .to_string()
438 .contains("Unterminated block comment"));
439 }
440
441 #[test]
442 fn test_parse_complex_script() {
443 let content = r#"
444-- Create table
445CREATE TABLE users (
446 id INT PRIMARY KEY,
447 name TEXT,
448 email TEXT
449);
450
451/* Insert test data */
452INSERT INTO users VALUES (1, 'Alice', 'alice@example.com');
453INSERT INTO users VALUES (2, 'Bob', 'bob@example.com');
454
455-- Query with string containing semicolon
456SELECT * FROM users WHERE email = 'test;email@example.com';
457"#;
458 let result = parse_script(content);
459 assert!(result.is_ok());
460 let statements = result.unwrap();
461 assert_eq!(statements.len(), 4);
462 }
463
464 #[test]
465 fn test_parse_empty_statements() {
466 let content = ";;; SELECT * FROM users; ;;;";
467 let result = parse_script(content);
468 assert!(result.is_ok());
469 let statements = result.unwrap();
470 assert_eq!(statements.len(), 1);
471 assert_eq!(statements[0], "SELECT * FROM users");
472 }
473
474 #[test]
475 fn test_parse_mixed_quotes() {
476 let content = r#"INSERT INTO users VALUES (1, 'single', "double");"#;
477 let result = parse_script(content);
478 assert!(result.is_ok());
479 let statements = result.unwrap();
480 assert_eq!(statements.len(), 1);
481 }
482
483 #[test]
484 fn test_parse_comment_in_string() {
485 let content = "INSERT INTO users VALUES (1, 'value with -- comment inside');";
486 let result = parse_script(content);
487 assert!(result.is_ok());
488 let statements = result.unwrap();
489 assert_eq!(statements.len(), 1);
490 assert_eq!(
491 statements[0],
492 "INSERT INTO users VALUES (1, 'value with -- comment inside')"
493 );
494 }
495
496 #[test]
497 fn test_parse_block_comment_in_string() {
498 let content = "INSERT INTO users VALUES (1, 'value with /* comment */ inside');";
499 let result = parse_script(content);
500 assert!(result.is_ok());
501 let statements = result.unwrap();
502 assert_eq!(statements.len(), 1);
503 assert_eq!(
504 statements[0],
505 "INSERT INTO users VALUES (1, 'value with /* comment */ inside')"
506 );
507 }
508}