use crate::batch::spec::{BatchOperation, BatchSpec, DeleteOp, ExecutionMode, PatchOp, RenameOp};
use crate::batch::transaction::{BatchTransaction, RollbackMode, TransactionResult};
use crate::error::{Result, SpliceError};
use crate::graph::{CodeGraph, MagellanIntegration};
use crate::symbol::Symbol;
use std::path::{Path, PathBuf};
use std::time::Instant;
#[derive(Debug, Clone)]
pub struct OperationResult {
pub index: usize,
pub op_type: String,
pub success: bool,
pub error: Option<String>,
pub duration_ms: u64,
}
#[derive(Debug, Clone)]
pub struct BatchResult {
pub spec_path: PathBuf,
pub total_operations: usize,
pub successful: usize,
pub failed: usize,
pub operations: Vec<OperationResult>,
pub total_duration_ms: u64,
pub stopped_early: bool,
}
pub struct BatchExecutor {
dry_run: bool,
db_path: Option<PathBuf>,
analyzer_mode: crate::validate::AnalyzerMode,
}
impl BatchExecutor {
pub fn new(
dry_run: bool,
db_path: Option<PathBuf>,
analyzer_mode: crate::validate::AnalyzerMode,
) -> Self {
Self {
dry_run,
db_path,
analyzer_mode,
}
}
pub fn execute(&mut self, spec: &BatchSpec) -> Result<BatchResult> {
let start = Instant::now();
let total_operations = spec.operations.len();
let mut operations = Vec::with_capacity(total_operations);
let mut successful = 0;
let mut failed = 0;
let mut stopped_early = false;
for (index, op) in spec.operations.iter().enumerate() {
let op_start = Instant::now();
let op_index = index + 1; let op_type = self.operation_type_name(op);
let result = self.execute_operation(op, op_index, &spec.mode);
let duration_ms = op_start.elapsed().as_millis() as u64;
let is_success = result.is_ok();
let error_msg = result.err().map(|e| e.to_string());
let op_result = OperationResult {
index: op_index,
op_type: op_type.clone(),
success: is_success,
error: error_msg,
duration_ms,
};
self.report_progress(&op_result);
match (is_success, spec.mode) {
(true, _) => successful += 1,
(false, ExecutionMode::StopOnError) => {
failed += 1;
stopped_early = true;
}
(false, ExecutionMode::ContinueOnError) => {
failed += 1;
}
}
operations.push(op_result);
if stopped_early {
break;
}
}
let total_duration_ms = start.elapsed().as_millis() as u64;
Ok(BatchResult {
spec_path: PathBuf::from("<unknown>"), total_operations,
successful,
failed,
operations,
total_duration_ms,
stopped_early,
})
}
pub fn execute_transaction(
&mut self,
spec: &BatchSpec,
dry_run: bool,
rollback_mode: RollbackMode,
) -> Result<TransactionResult>
where
Self: Sized,
{
let db_path = self.db_path.as_ref().ok_or_else(|| {
SpliceError::Other("Transaction requires database path for snapshots".to_string())
})?;
let transaction = BatchTransaction::new(
db_path.clone(),
rollback_mode,
true, self.analyzer_mode.clone(),
);
transaction.execute(spec, dry_run)
}
fn execute_operation(
&mut self,
op: &BatchOperation,
index: usize,
_mode: &ExecutionMode,
) -> Result<()> {
match op {
BatchOperation::Patch(patch_op) => self.execute_patch(patch_op, index),
BatchOperation::Delete(delete_op) => self.execute_delete(delete_op, index),
BatchOperation::Rename(rename_op) => self.execute_rename(rename_op, index),
}
}
fn execute_patch(&mut self, op: &PatchOp, index: usize) -> Result<()> {
if op.snapshot_before {
if let Some(db_path) = &self.db_path {
self.capture_snapshot(db_path, &format!("batch-patch-{}", index))?;
}
}
let replacement = std::fs::read_to_string(&op.with).map_err(|e| {
SpliceError::Other(format!(
"Failed to read replacement file '{}': {}",
op.with.display(),
e
))
})?;
let db_path = self.db_path.as_ref().ok_or_else(|| {
SpliceError::Other(
"Batch patch operations require --db flag for symbol resolution".to_string(),
)
})?;
let mut code_graph = CodeGraph::open(db_path)?;
let source = std::fs::read(&op.file)?;
let language =
crate::symbol::Language::from_path(&op.file).ok_or_else(|| SpliceError::Parse {
file: op.file.clone(),
message: "Cannot detect language - unknown file extension".to_string(),
})?;
let symbols = crate::ingest::extract_symbols_with_language(&op.file, &source, language)?;
let file_path = crate::resolve::normalize_lookup_path(&op.file);
for symbol in &symbols {
code_graph.store_symbol_with_file_and_language(
&file_path,
symbol.name(),
symbol.kind(),
symbol.language(),
symbol.byte_start(),
symbol.byte_end(),
symbol.line_start(),
symbol.line_end(),
symbol.col_start(),
symbol.col_end(),
)?;
}
let kind_str = op.kind.as_deref();
let resolved =
crate::resolve::resolve_symbol(&code_graph, Some(&file_path), kind_str, &op.symbol)?;
let workspace_dir = file_path.parent().ok_or_else(|| {
SpliceError::Other("Cannot determine workspace directory".to_string())
})?;
if self.dry_run {
eprintln!(
"[PREVIEW] Would patch {}::{} in file: {}",
kind_str.unwrap_or("symbol"),
op.symbol,
file_path.display()
);
Ok(())
} else {
crate::patch::apply_patch_with_validation(
&file_path,
resolved.byte_start,
resolved.byte_end,
&replacement,
workspace_dir,
language,
self.analyzer_mode.clone(),
false, false, )?;
Ok(())
}
}
fn execute_delete(&mut self, op: &DeleteOp, index: usize) -> Result<()> {
if op.snapshot_before {
if let Some(db_path) = &self.db_path {
self.capture_snapshot(db_path, &format!("batch-delete-{}", index))?;
}
}
let db_path = self.db_path.as_ref().ok_or_else(|| {
SpliceError::Other(
"Batch delete operations require --db flag for symbol resolution".to_string(),
)
})?;
let mut code_graph = CodeGraph::open(db_path)?;
let source = std::fs::read(&op.file)?;
let language =
crate::symbol::Language::from_path(&op.file).ok_or_else(|| SpliceError::Parse {
file: op.file.clone(),
message: "Cannot detect language - unknown file extension".to_string(),
})?;
let symbols = crate::ingest::extract_symbols_with_language(&op.file, &source, language)?;
let file_path = crate::resolve::normalize_lookup_path(&op.file);
for symbol in &symbols {
code_graph.store_symbol_with_file_and_language(
&file_path,
symbol.name(),
symbol.kind(),
symbol.language(),
symbol.byte_start(),
symbol.byte_end(),
symbol.line_start(),
symbol.line_end(),
symbol.col_start(),
symbol.col_end(),
)?;
}
let kind_str = op.kind.as_deref();
let resolved =
crate::resolve::resolve_symbol(&code_graph, Some(&file_path), kind_str, &op.symbol)?;
let workspace_dir = file_path.parent().ok_or_else(|| {
SpliceError::Other("Cannot determine workspace directory".to_string())
})?;
if self.dry_run {
eprintln!(
"[PREVIEW] Would delete {}::{} in file: {}",
kind_str.unwrap_or("symbol"),
op.symbol,
file_path.display()
);
Ok(())
} else {
crate::patch::apply_patch_with_validation(
&file_path,
resolved.byte_start,
resolved.byte_end,
"", workspace_dir,
language,
self.analyzer_mode.clone(),
false, false, )?;
Ok(())
}
}
fn execute_rename(&mut self, op: &RenameOp, index: usize) -> Result<()> {
if op.snapshot_before {
if let Some(db_path) = &self.db_path {
self.capture_snapshot(db_path, &format!("batch-rename-{}", index))?;
}
}
let db_path = self.db_path.as_ref().ok_or_else(|| {
SpliceError::Other("Batch rename operations require --db flag".to_string())
})?;
let mut magellan = MagellanIntegration::open(db_path)?;
let mut matches = magellan.find_symbol_by_name(&op.from, true)?;
let file_path_str = op.file.to_string_lossy().to_string();
matches.retain(|s| s.file_path == file_path_str);
if matches.is_empty() {
return Err(SpliceError::Other(format!(
"Symbol '{}' not found in file '{}'",
op.from,
op.file.display()
)));
}
let symbol_info = matches.into_iter().next().unwrap();
let references = magellan.get_all_references(symbol_info.entity_id)?;
if references.is_empty() {
eprintln!("Warning: No references found for symbol '{}'", op.from);
}
let filtered_refs = if let Some(ref files) = op.files {
let file_set: std::collections::HashSet<String> = files
.iter()
.map(|p| p.to_string_lossy().to_string())
.collect();
references
.into_iter()
.filter(|r| file_set.contains(&r.file_path.to_string_lossy().to_string()))
.collect()
} else {
references
};
if self.dry_run {
eprintln!(
"[PREVIEW] Would rename '{}' to '{}' in {} file(s)",
op.from,
op.to,
filtered_refs.len()
);
Ok(())
} else {
let grouped = crate::graph::rename::group_references_by_file(&filtered_refs);
for (file_path, refs) in grouped {
crate::graph::rename::apply_replacements_in_file(
&file_path, &op.from, &op.to, &refs,
)?;
}
Ok(())
}
}
fn capture_snapshot(&self, db_path: &Path, operation: &str) -> Result<()> {
use crate::proof::generation::generate_snapshot;
use crate::proof::storage::SnapshotStorage;
let storage = SnapshotStorage::new()?;
let snapshot = generate_snapshot(db_path)?;
storage.save_snapshot(operation, db_path, snapshot)?;
Ok(())
}
fn operation_type_name(&self, op: &BatchOperation) -> String {
match op {
BatchOperation::Patch(_) => "patch".to_string(),
BatchOperation::Delete(_) => "delete".to_string(),
BatchOperation::Rename(_) => "rename".to_string(),
}
}
fn report_progress(&self, result: &OperationResult) {
let status = if result.success { "+" } else { "x" };
eprintln!(
"[{}] Op {}/{}: {} ({})",
status,
result.index,
"?", result.op_type,
result.duration_ms
);
if let Some(error) = &result.error {
eprintln!(" Error: {}", error);
}
}
}