use std::time::Instant;
use vibesql_parser::Parser;
use vibesql_storage::Database;
mod copy_handler;
pub mod display;
pub mod validation;
#[cfg(test)]
mod tests;
pub struct SqlExecutor {
db: Database,
timing_enabled: bool,
}
#[derive(Debug, Clone)]
pub struct QueryResult {
pub rows: Vec<Vec<String>>,
pub columns: Vec<String>,
pub row_count: usize,
pub execution_time_ms: Option<f64>,
}
impl SqlExecutor {
pub fn new(database: Option<String>) -> anyhow::Result<Self> {
let db = if let Some(db_path) = database {
if std::path::Path::new(&db_path).exists() {
vibesql_executor::load_sql_dump(&db_path)
.map_err(|e| anyhow::anyhow!("Failed to load database: {}", e))?
} else {
Database::new()
}
} else {
Database::new()
};
Ok(SqlExecutor { db, timing_enabled: false })
}
pub fn execute(&mut self, sql: &str) -> anyhow::Result<QueryResult> {
let start = Instant::now();
let statement = Parser::parse_sql(sql).map_err(|e| anyhow::anyhow!("{}", e))?;
let mut result = QueryResult {
rows: Vec::new(),
columns: Vec::new(),
row_count: 0,
execution_time_ms: None,
};
match statement {
vibesql_ast::Statement::Select(select_stmt) => {
let executor = vibesql_executor::SelectExecutor::new(&self.db);
match executor.execute(&select_stmt) {
Ok(rows) => {
result.row_count = rows.len();
if !rows.is_empty() {
result.columns = vec!["Column".to_string(); rows[0].values.len()];
for row in rows {
let row_strs: Vec<String> =
row.values.iter().map(|v| format!("{:?}", v)).collect();
result.rows.push(row_strs);
}
}
}
Err(e) => return Err(anyhow::anyhow!("{}", e)),
}
}
vibesql_ast::Statement::CreateTable(create_stmt) => {
match vibesql_executor::CreateTableExecutor::execute(&create_stmt, &mut self.db) {
Ok(_) => {
result.row_count = 0; }
Err(e) => return Err(anyhow::anyhow!("{}", e)),
}
}
vibesql_ast::Statement::Insert(insert_stmt) => {
match vibesql_executor::InsertExecutor::execute(&mut self.db, &insert_stmt) {
Ok(affected_rows) => {
result.row_count = affected_rows;
}
Err(e) => return Err(anyhow::anyhow!("{}", e)),
}
}
vibesql_ast::Statement::Update(update_stmt) => {
match vibesql_executor::UpdateExecutor::execute(&update_stmt, &mut self.db) {
Ok(affected_rows) => {
result.row_count = affected_rows;
}
Err(e) => return Err(anyhow::anyhow!("{}", e)),
}
}
vibesql_ast::Statement::Delete(delete_stmt) => {
match vibesql_executor::DeleteExecutor::execute(&delete_stmt, &mut self.db) {
Ok(affected_rows) => {
result.row_count = affected_rows;
}
Err(e) => return Err(anyhow::anyhow!("{}", e)),
}
}
vibesql_ast::Statement::CreateView(view_stmt) => {
match vibesql_executor::advanced_objects::execute_create_view(
&view_stmt,
&mut self.db,
) {
Ok(_) => {
result.row_count = 0; }
Err(e) => return Err(anyhow::anyhow!("{}", e)),
}
}
vibesql_ast::Statement::DropView(drop_stmt) => {
match vibesql_executor::advanced_objects::execute_drop_view(
&drop_stmt,
&mut self.db,
) {
Ok(_) => {
result.row_count = 0; }
Err(e) => return Err(anyhow::anyhow!("{}", e)),
}
}
vibesql_ast::Statement::DropTable(drop_stmt) => {
match vibesql_executor::DropTableExecutor::execute(&drop_stmt, &mut self.db) {
Ok(_) => {
result.row_count = 0; }
Err(e) => return Err(anyhow::anyhow!("{}", e)),
}
}
vibesql_ast::Statement::TruncateTable(truncate_stmt) => {
match vibesql_executor::TruncateTableExecutor::execute(&truncate_stmt, &mut self.db)
{
Ok(rows_deleted) => {
result.row_count = rows_deleted;
}
Err(e) => return Err(anyhow::anyhow!("{}", e)),
}
}
vibesql_ast::Statement::CreateTrigger(trigger_stmt) => {
match vibesql_executor::TriggerExecutor::create_trigger(&mut self.db, &trigger_stmt)
{
Ok(_) => {
result.row_count = 0; }
Err(e) => return Err(anyhow::anyhow!("{}", e)),
}
}
vibesql_ast::Statement::AlterTrigger(alter_stmt) => {
match vibesql_executor::TriggerExecutor::alter_trigger(&mut self.db, &alter_stmt) {
Ok(_) => {
result.row_count = 0; }
Err(e) => return Err(anyhow::anyhow!("{}", e)),
}
}
vibesql_ast::Statement::DropTrigger(drop_stmt) => {
match vibesql_executor::TriggerExecutor::drop_trigger(&mut self.db, &drop_stmt) {
Ok(_) => {
result.row_count = 0; }
Err(e) => return Err(anyhow::anyhow!("{}", e)),
}
}
vibesql_ast::Statement::SetVariable(set_var_stmt) => {
match vibesql_executor::SchemaExecutor::execute_set_variable(
&set_var_stmt,
&mut self.db,
) {
Ok(_) => {
result.row_count = 0;
}
Err(e) => return Err(anyhow::anyhow!("{}", e)),
}
}
vibesql_ast::Statement::Reindex(reindex_stmt) => {
match vibesql_executor::ReindexExecutor::execute(&reindex_stmt, &self.db) {
Ok(msg) => {
println!("{}", msg);
result.row_count = 0; }
Err(e) => return Err(anyhow::anyhow!("{}", e)),
}
}
vibesql_ast::Statement::Analyze(analyze_stmt) => {
match vibesql_executor::AnalyzeExecutor::execute(&analyze_stmt, &mut self.db) {
Ok(msg) => {
println!("{}", msg);
result.row_count = 0; }
Err(e) => return Err(anyhow::anyhow!("{}", e)),
}
}
vibesql_ast::Statement::Explain(explain_stmt) => {
match vibesql_executor::ExplainExecutor::execute(&explain_stmt, &self.db) {
Ok(explain_result) => {
let output = match explain_stmt.format {
vibesql_ast::ExplainFormat::Text => explain_result.to_text(),
vibesql_ast::ExplainFormat::Json => explain_result.to_json(),
};
result.columns = vec!["QUERY PLAN".to_string()];
for line in output.lines() {
result.rows.push(vec![line.to_string()]);
}
result.row_count = result.rows.len();
}
Err(e) => return Err(anyhow::anyhow!("{}", e)),
}
}
vibesql_ast::Statement::CreateIndex(index_stmt) => {
match vibesql_executor::CreateIndexExecutor::execute(&index_stmt, &mut self.db) {
Ok(msg) => {
println!("{}", msg);
result.row_count = 0; }
Err(e) => return Err(anyhow::anyhow!("{}", e)),
}
}
vibesql_ast::Statement::DropIndex(drop_stmt) => {
match vibesql_executor::DropIndexExecutor::execute(&drop_stmt, &mut self.db) {
Ok(msg) => {
println!("{}", msg);
result.row_count = 0; }
Err(e) => return Err(anyhow::anyhow!("{}", e)),
}
}
vibesql_ast::Statement::AlterTable(alter_stmt) => {
match vibesql_executor::AlterTableExecutor::execute(&alter_stmt, &mut self.db) {
Ok(msg) => {
println!("{}", msg);
result.row_count = 0; }
Err(e) => return Err(anyhow::anyhow!("{}", e)),
}
}
vibesql_ast::Statement::BeginTransaction(begin_stmt) => {
match vibesql_executor::BeginTransactionExecutor::execute(&begin_stmt, &mut self.db)
{
Ok(msg) => {
println!("{}", msg);
result.row_count = 0;
}
Err(e) => return Err(anyhow::anyhow!("{}", e)),
}
}
vibesql_ast::Statement::Commit(commit_stmt) => {
match vibesql_executor::CommitExecutor::execute(&commit_stmt, &mut self.db) {
Ok(msg) => {
println!("{}", msg);
result.row_count = 0;
}
Err(e) => return Err(anyhow::anyhow!("{}", e)),
}
}
vibesql_ast::Statement::Rollback(rollback_stmt) => {
match vibesql_executor::RollbackExecutor::execute(&rollback_stmt, &mut self.db) {
Ok(msg) => {
println!("{}", msg);
result.row_count = 0;
}
Err(e) => return Err(anyhow::anyhow!("{}", e)),
}
}
vibesql_ast::Statement::Savepoint(savepoint_stmt) => {
match vibesql_executor::SavepointExecutor::execute(&savepoint_stmt, &mut self.db) {
Ok(msg) => {
println!("{}", msg);
result.row_count = 0;
}
Err(e) => return Err(anyhow::anyhow!("{}", e)),
}
}
vibesql_ast::Statement::RollbackToSavepoint(rollback_stmt) => {
match vibesql_executor::RollbackToSavepointExecutor::execute(
&rollback_stmt,
&mut self.db,
) {
Ok(msg) => {
println!("{}", msg);
result.row_count = 0;
}
Err(e) => return Err(anyhow::anyhow!("{}", e)),
}
}
vibesql_ast::Statement::ReleaseSavepoint(release_stmt) => {
match vibesql_executor::ReleaseSavepointExecutor::execute(
&release_stmt,
&mut self.db,
) {
Ok(msg) => {
println!("{}", msg);
result.row_count = 0;
}
Err(e) => return Err(anyhow::anyhow!("{}", e)),
}
}
vibesql_ast::Statement::ShowTables(show_stmt) => {
result = self.execute_show_tables(&show_stmt)?;
}
vibesql_ast::Statement::ShowDatabases(show_stmt) => {
result = self.execute_show_databases(&show_stmt)?;
}
vibesql_ast::Statement::ShowColumns(show_stmt) => {
result = self.execute_show_columns(&show_stmt)?;
}
vibesql_ast::Statement::ShowIndex(show_stmt) => {
result = self.execute_show_index(&show_stmt)?;
}
vibesql_ast::Statement::ShowCreateTable(show_stmt) => {
result = self.execute_show_create_table(&show_stmt)?;
}
vibesql_ast::Statement::Describe(desc_stmt) => {
result = self.execute_describe(&desc_stmt)?;
}
_ => {
return Err(anyhow::anyhow!("Statement type not yet supported in CLI"));
}
}
let elapsed = start.elapsed().as_secs_f64() * 1000.0;
if self.timing_enabled {
result.execution_time_ms = Some(elapsed);
}
Ok(result)
}
pub fn toggle_timing(&mut self) {
self.timing_enabled = !self.timing_enabled;
let state = if self.timing_enabled { "on" } else { "off" };
println!("Timing is {}", state);
}
pub fn save_database(&self, path: &str) -> anyhow::Result<()> {
self.db
.save_sql_dump(path)
.map_err(|e| anyhow::anyhow!("Failed to save database to {}: {}", path, e))
}
fn execute_show_tables(
&self,
stmt: &vibesql_ast::ShowTablesStmt,
) -> anyhow::Result<QueryResult> {
let tables = self.db.list_tables();
let filtered_tables: Vec<String> = if let Some(pattern) = &stmt.like_pattern {
let regex_pattern = like_to_regex(pattern);
let re = regex::Regex::new(®ex_pattern)
.map_err(|e| anyhow::anyhow!("Invalid LIKE pattern: {}", e))?;
tables.into_iter().filter(|t| re.is_match(t)).collect()
} else {
tables
};
let rows: Vec<Vec<String>> = filtered_tables.iter().map(|t| vec![t.clone()]).collect();
let row_count = rows.len();
Ok(QueryResult {
columns: vec!["Tables_in_database".to_string()],
rows,
row_count,
execution_time_ms: None,
})
}
fn execute_show_databases(
&self,
stmt: &vibesql_ast::ShowDatabasesStmt,
) -> anyhow::Result<QueryResult> {
let schemas = self.db.catalog.list_schemas();
let filtered_schemas: Vec<String> = if let Some(pattern) = &stmt.like_pattern {
let regex_pattern = like_to_regex(pattern);
let re = regex::Regex::new(®ex_pattern)
.map_err(|e| anyhow::anyhow!("Invalid LIKE pattern: {}", e))?;
schemas.into_iter().filter(|s| re.is_match(s)).collect()
} else {
schemas
};
let rows: Vec<Vec<String>> = filtered_schemas.iter().map(|s| vec![s.clone()]).collect();
let row_count = rows.len();
Ok(QueryResult {
columns: vec!["Database".to_string()],
rows,
row_count,
execution_time_ms: None,
})
}
fn execute_show_columns(
&self,
stmt: &vibesql_ast::ShowColumnsStmt,
) -> anyhow::Result<QueryResult> {
let normalized_name = stmt.table_name.to_uppercase();
let table = self
.db
.get_table(&normalized_name)
.ok_or_else(|| anyhow::anyhow!("Table '{}' does not exist", stmt.table_name))?;
let mut rows: Vec<Vec<String>> = Vec::new();
for column in &table.schema.columns {
if let Some(pattern) = &stmt.like_pattern {
let regex_pattern = like_to_regex(pattern);
let re = regex::Regex::new(®ex_pattern)
.map_err(|e| anyhow::anyhow!("Invalid LIKE pattern: {}", e))?;
if !re.is_match(&column.name) {
continue;
}
}
let nullable = if column.nullable { "YES" } else { "NO" };
let default_val =
column.default_value.as_ref().map(|v| format!("{:?}", v)).unwrap_or_default();
let key = if table
.schema
.primary_key
.as_ref()
.map(|pk| pk.contains(&column.name))
.unwrap_or(false)
{
"PRI"
} else {
""
};
let row = if stmt.full {
vec![
column.name.clone(),
display::format_data_type(&column.data_type),
String::new(), nullable.to_string(),
key.to_string(),
default_val,
String::new(), String::new(), String::new(), ]
} else {
vec![
column.name.clone(),
display::format_data_type(&column.data_type),
nullable.to_string(),
key.to_string(),
default_val,
String::new(), ]
};
rows.push(row);
}
let row_count = rows.len();
let columns = if stmt.full {
vec![
"Field".to_string(),
"Type".to_string(),
"Collation".to_string(),
"Null".to_string(),
"Key".to_string(),
"Default".to_string(),
"Extra".to_string(),
"Privileges".to_string(),
"Comment".to_string(),
]
} else {
vec![
"Field".to_string(),
"Type".to_string(),
"Null".to_string(),
"Key".to_string(),
"Default".to_string(),
"Extra".to_string(),
]
};
Ok(QueryResult { columns, rows, row_count, execution_time_ms: None })
}
fn execute_show_index(&self, stmt: &vibesql_ast::ShowIndexStmt) -> anyhow::Result<QueryResult> {
let normalized_name = stmt.table_name.to_uppercase();
let _ = self
.db
.get_table(&normalized_name)
.ok_or_else(|| anyhow::anyhow!("Table '{}' does not exist", stmt.table_name))?;
let index_names = self.db.list_indexes();
let mut rows: Vec<Vec<String>> = Vec::new();
for index_name in index_names {
if let Some(index_meta) = self.db.get_index(&index_name) {
if index_meta.table_name == normalized_name {
for (seq, col) in index_meta.columns.iter().enumerate() {
rows.push(vec![
normalized_name.clone(), if index_meta.unique { "0" } else { "1" }.to_string(), index_meta.index_name.clone(), (seq + 1).to_string(), col.column_name.clone(), "A".to_string(), String::new(), String::new(), String::new(), String::new(), "BTREE".to_string(), String::new(), ]);
}
}
}
}
let row_count = rows.len();
Ok(QueryResult {
columns: vec![
"Table".to_string(),
"Non_unique".to_string(),
"Key_name".to_string(),
"Seq_in_index".to_string(),
"Column_name".to_string(),
"Collation".to_string(),
"Cardinality".to_string(),
"Sub_part".to_string(),
"Packed".to_string(),
"Null".to_string(),
"Index_type".to_string(),
"Comment".to_string(),
],
rows,
row_count,
execution_time_ms: None,
})
}
fn execute_show_create_table(
&self,
stmt: &vibesql_ast::ShowCreateTableStmt,
) -> anyhow::Result<QueryResult> {
let normalized_name = stmt.table_name.to_uppercase();
let table = self
.db
.get_table(&normalized_name)
.ok_or_else(|| anyhow::anyhow!("Table '{}' does not exist", stmt.table_name))?;
let mut create_sql = format!("CREATE TABLE {} (\n", normalized_name);
let mut column_defs: Vec<String> = Vec::new();
for column in &table.schema.columns {
let mut def =
format!(" {} {}", column.name, display::format_data_type(&column.data_type));
if !column.nullable {
def.push_str(" NOT NULL");
}
if let Some(default) = &column.default_value {
def.push_str(&format!(" DEFAULT {:?}", default));
}
column_defs.push(def);
}
if let Some(pk_cols) = &table.schema.primary_key {
column_defs.push(format!(" PRIMARY KEY ({})", pk_cols.join(", ")));
}
for unique_cols in &table.schema.unique_constraints {
column_defs.push(format!(" UNIQUE ({})", unique_cols.join(", ")));
}
for fk in &table.schema.foreign_keys {
column_defs.push(format!(
" FOREIGN KEY ({}) REFERENCES {}({})",
fk.column_names.join(", "),
fk.parent_table,
fk.parent_column_names.join(", ")
));
}
create_sql.push_str(&column_defs.join(",\n"));
create_sql.push_str("\n)");
Ok(QueryResult {
columns: vec!["Table".to_string(), "Create Table".to_string()],
rows: vec![vec![normalized_name, create_sql]],
row_count: 1,
execution_time_ms: None,
})
}
fn execute_describe(&self, stmt: &vibesql_ast::DescribeStmt) -> anyhow::Result<QueryResult> {
let show_stmt = vibesql_ast::ShowColumnsStmt {
table_name: stmt.table_name.clone(),
database: None,
full: false,
like_pattern: stmt.column_pattern.clone(),
where_clause: None,
};
self.execute_show_columns(&show_stmt)
}
}
fn like_to_regex(pattern: &str) -> String {
let mut regex = String::from("^");
for ch in pattern.chars() {
match ch {
'%' => regex.push_str(".*"),
'_' => regex.push('.'),
'.' | '+' | '*' | '?' | '^' | '$' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '\\' => {
regex.push('\\');
regex.push(ch);
}
_ => regex.push(ch),
}
}
regex.push('$');
regex
}