use anyhow::{Context, Result};
use cqlite_core::Database;
use std::path::Path;
use crate::cli::OutputFormat;
use crate::config::OutputConfig;
use crate::error::{print_error, CliExitCode};
#[cfg(feature = "state_machine")]
pub async fn execute_script_file(
file_path: &Path,
database: &Database,
output_config: &OutputConfig,
format: OutputFormat,
) -> Result<()> {
let statements = load_script(file_path)
.with_context(|| format!("Failed to parse script file: {}", file_path.display()))?;
if statements.is_empty() {
eprintln!("Warning: Script file contains no statements");
return Ok(());
}
println!(
"Executing {} statement(s) from {}",
statements.len(),
file_path.display()
);
for (index, statement) in statements.iter().enumerate() {
let statement_num = index + 1;
if let Err(e) = crate::commands::execute_query(
database,
statement,
false, false, format.clone(),
output_config,
)
.await
{
eprintln!(
"Error executing statement {} in {}",
statement_num,
file_path.display()
);
eprintln!("Statement: {}", statement);
print_error(&e, CliExitCode::QueryExecutionError);
std::process::exit(CliExitCode::QueryExecutionError.as_i32());
}
}
println!("\nSuccessfully executed {} statement(s)", statements.len());
Ok(())
}
#[cfg(not(feature = "state_machine"))]
pub async fn execute_script_file(
_file_path: &Path,
_database: &Database,
_output_config: &OutputConfig,
_format: OutputFormat,
) -> Result<()> {
anyhow::bail!(
"Script execution is not available in M1.\n\
Build with --features state_machine to enable this feature.\n\
See CLAUDE.md for M1 API examples."
)
}
pub fn parse_script(script_content: &str) -> Result<Vec<String>> {
let mut statements = Vec::new();
let mut current_statement = String::new();
let mut chars = script_content.chars().peekable();
let mut in_string = false;
let mut in_line_comment = false;
let mut in_block_comment = false;
let mut string_delimiter = '\0';
while let Some(ch) = chars.next() {
if !in_string && !in_block_comment && ch == '-' {
if chars.peek() == Some(&'-') {
chars.next(); in_line_comment = true;
continue;
}
}
if in_line_comment {
if ch == '\n' {
in_line_comment = false;
current_statement.push(ch);
}
continue;
}
if !in_string && !in_line_comment && ch == '/' {
if chars.peek() == Some(&'*') {
chars.next(); in_block_comment = true;
continue;
}
}
if in_block_comment {
if ch == '*' && chars.peek() == Some(&'/') {
chars.next(); in_block_comment = false;
}
continue;
}
if !in_block_comment && !in_line_comment {
if ch == '\'' || ch == '"' {
if !in_string {
in_string = true;
string_delimiter = ch;
current_statement.push(ch);
} else if ch == string_delimiter {
if chars.peek() == Some(&ch) {
current_statement.push(ch);
current_statement.push(chars.next().unwrap());
} else {
in_string = false;
current_statement.push(ch);
}
} else {
current_statement.push(ch);
}
continue;
}
}
if !in_string && !in_line_comment && !in_block_comment && ch == ';' {
let trimmed = current_statement.trim();
if !trimmed.is_empty() {
statements.push(trimmed.to_string());
}
current_statement.clear();
continue;
}
if !in_line_comment && !in_block_comment {
current_statement.push(ch);
}
}
if in_string {
anyhow::bail!("Unterminated string literal in script");
}
if in_block_comment {
anyhow::bail!("Unterminated block comment in script");
}
let remaining = current_statement.trim();
if !remaining.is_empty() {
anyhow::bail!(
"Unterminated statement found (missing semicolon): {}",
if remaining.len() > 50 {
format!("{}...", &remaining[..50])
} else {
remaining.to_string()
}
);
}
Ok(statements)
}
pub fn load_script(script_path: &Path) -> Result<Vec<String>> {
let content = std::fs::read_to_string(script_path)
.with_context(|| format!("Failed to read script file: {}", script_path.display()))?;
parse_script(&content)
.with_context(|| format!("Failed to parse script file: {}", script_path.display()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_empty_file() {
let result = parse_script("");
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 0);
}
#[test]
fn test_parse_single_statement() {
let content = "SELECT * FROM users;";
let result = parse_script(content);
assert!(result.is_ok());
let statements = result.unwrap();
assert_eq!(statements.len(), 1);
assert_eq!(statements[0], "SELECT * FROM users");
}
#[test]
fn test_parse_multiple_statements() {
let content = "SELECT * FROM users;\nINSERT INTO users VALUES (1, 'test');";
let result = parse_script(content);
assert!(result.is_ok());
let statements = result.unwrap();
assert_eq!(statements.len(), 2);
assert_eq!(statements[0], "SELECT * FROM users");
assert_eq!(statements[1], "INSERT INTO users VALUES (1, 'test')");
}
#[test]
fn test_parse_line_comments() {
let content = "-- This is a comment\nSELECT * FROM users; -- inline comment";
let result = parse_script(content);
assert!(result.is_ok());
let statements = result.unwrap();
assert_eq!(statements.len(), 1);
assert_eq!(statements[0], "SELECT * FROM users");
}
#[test]
fn test_parse_block_comments() {
let content = "/* block comment */ SELECT * FROM users; /* another comment */";
let result = parse_script(content);
assert!(result.is_ok());
let statements = result.unwrap();
assert_eq!(statements.len(), 1);
assert_eq!(statements[0], "SELECT * FROM users");
}
#[test]
fn test_parse_multiline_block_comment() {
let content = "/*\n * Multi-line\n * block comment\n */\nSELECT * FROM users;";
let result = parse_script(content);
assert!(result.is_ok());
let statements = result.unwrap();
assert_eq!(statements.len(), 1);
assert_eq!(statements[0], "SELECT * FROM users");
}
#[test]
fn test_parse_string_with_semicolon() {
let content = "INSERT INTO users VALUES (1, 'test;value');";
let result = parse_script(content);
assert!(result.is_ok());
let statements = result.unwrap();
assert_eq!(statements.len(), 1);
assert_eq!(statements[0], "INSERT INTO users VALUES (1, 'test;value')");
}
#[test]
fn test_parse_double_quoted_string() {
let content = r#"INSERT INTO users VALUES (1, "test;value");"#;
let result = parse_script(content);
assert!(result.is_ok());
let statements = result.unwrap();
assert_eq!(statements.len(), 1);
assert_eq!(
statements[0],
r#"INSERT INTO users VALUES (1, "test;value")"#
);
}
#[test]
fn test_parse_escaped_single_quotes() {
let content = "INSERT INTO users VALUES (1, 'test''s value');";
let result = parse_script(content);
assert!(result.is_ok());
let statements = result.unwrap();
assert_eq!(statements.len(), 1);
assert_eq!(
statements[0],
"INSERT INTO users VALUES (1, 'test''s value')"
);
}
#[test]
fn test_parse_escaped_double_quotes() {
let content = r#"INSERT INTO users VALUES (1, "test""s value");"#;
let result = parse_script(content);
assert!(result.is_ok());
let statements = result.unwrap();
assert_eq!(statements.len(), 1);
assert_eq!(
statements[0],
r#"INSERT INTO users VALUES (1, "test""s value")"#
);
}
#[test]
fn test_parse_multiline_statement() {
let content = "SELECT *\nFROM users\nWHERE id = 1;";
let result = parse_script(content);
assert!(result.is_ok());
let statements = result.unwrap();
assert_eq!(statements.len(), 1);
assert_eq!(statements[0], "SELECT *\nFROM users\nWHERE id = 1");
}
#[test]
fn test_parse_blank_lines() {
let content = "SELECT * FROM users;\n\n\nINSERT INTO users VALUES (1, 'test');";
let result = parse_script(content);
assert!(result.is_ok());
let statements = result.unwrap();
assert_eq!(statements.len(), 2);
}
#[test]
fn test_parse_comments_only() {
let content = "-- comment 1\n/* comment 2 */\n-- comment 3";
let result = parse_script(content);
assert!(result.is_ok());
assert_eq!(result.unwrap().len(), 0);
}
#[test]
fn test_parse_unterminated_statement() {
let content = "SELECT * FROM users";
let result = parse_script(content);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Unterminated statement"));
}
#[test]
fn test_parse_unterminated_string() {
let content = "INSERT INTO users VALUES (1, 'unterminated;";
let result = parse_script(content);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Unterminated string"));
}
#[test]
fn test_parse_unterminated_block_comment() {
let content = "/* unterminated comment\nSELECT * FROM users;";
let result = parse_script(content);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Unterminated block comment"));
}
#[test]
fn test_parse_complex_script() {
let content = r#"
-- Create table
CREATE TABLE users (
id INT PRIMARY KEY,
name TEXT,
email TEXT
);
/* Insert test data */
INSERT INTO users VALUES (1, 'Alice', 'alice@example.com');
INSERT INTO users VALUES (2, 'Bob', 'bob@example.com');
-- Query with string containing semicolon
SELECT * FROM users WHERE email = 'test;email@example.com';
"#;
let result = parse_script(content);
assert!(result.is_ok());
let statements = result.unwrap();
assert_eq!(statements.len(), 4);
}
#[test]
fn test_parse_empty_statements() {
let content = ";;; SELECT * FROM users; ;;;";
let result = parse_script(content);
assert!(result.is_ok());
let statements = result.unwrap();
assert_eq!(statements.len(), 1);
assert_eq!(statements[0], "SELECT * FROM users");
}
#[test]
fn test_parse_mixed_quotes() {
let content = r#"INSERT INTO users VALUES (1, 'single', "double");"#;
let result = parse_script(content);
assert!(result.is_ok());
let statements = result.unwrap();
assert_eq!(statements.len(), 1);
}
#[test]
fn test_parse_comment_in_string() {
let content = "INSERT INTO users VALUES (1, 'value with -- comment inside');";
let result = parse_script(content);
assert!(result.is_ok());
let statements = result.unwrap();
assert_eq!(statements.len(), 1);
assert_eq!(
statements[0],
"INSERT INTO users VALUES (1, 'value with -- comment inside')"
);
}
#[test]
fn test_parse_block_comment_in_string() {
let content = "INSERT INTO users VALUES (1, 'value with /* comment */ inside');";
let result = parse_script(content);
assert!(result.is_ok());
let statements = result.unwrap();
assert_eq!(statements.len(), 1);
assert_eq!(
statements[0],
"INSERT INTO users VALUES (1, 'value with /* comment */ inside')"
);
}
}