use clap::{Parser, Subcommand, ValueEnum};
use siftdb_core::{SiftDB, bench::SiftDBBenchmark, ingest::{Ingester, IngestOptions}};
use std::path::PathBuf;
use anyhow::Result;
#[derive(ValueEnum, Clone, Debug)]
enum OutputFormat {
Text,
Json,
}
#[derive(Parser)]
#[command(name = "sift")]
#[command(about = "SiftDB - Grep-Native Database for Code and Text Collections")]
#[command(version = "0.1.0")]
struct Cli {
#[arg(long, global = true, default_value = "300")]
lock_timeout: u64,
#[arg(long, global = true, default_value = "sift-cli")]
lock_holder: String,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
Init {
collection: PathBuf,
},
Import {
collection: PathBuf,
#[arg(long)]
from: PathBuf,
#[arg(long, action = clap::ArgAction::Append)]
include: Vec<String>,
#[arg(long, action = clap::ArgAction::Append)]
exclude: Vec<String>,
#[arg(long, default_value = "10485760")] max_file_bytes: u64,
},
Find {
collection: PathBuf,
query: String,
#[clap(short, long)]
limit: Option<usize>,
#[clap(short, long)]
path_glob: Option<String>,
#[clap(short, long, value_enum, default_value = "text")]
format: OutputFormat,
},
Regex {
collection: PathBuf,
pattern: String,
#[clap(short, long)]
limit: Option<usize>,
#[clap(short, long)]
path_glob: Option<String>,
#[clap(short, long, value_enum, default_value = "text")]
format: OutputFormat,
},
Open {
collection: PathBuf,
#[arg(long)]
file: String,
#[arg(long, default_value = "1")]
start_line: u32,
#[arg(long, default_value = "0")]
end_line: u32,
},
Benchmark {
collection: PathBuf,
#[arg(long)]
source: PathBuf,
#[arg(long, default_value = "text")]
format: String,
},
Compare {
path: PathBuf,
#[arg(long, default_value = "text")]
format: String,
},
Update {
collection: PathBuf,
#[arg(long)]
from: PathBuf,
#[arg(long, action = clap::ArgAction::Append)]
include: Vec<String>,
#[arg(long, action = clap::ArgAction::Append)]
exclude: Vec<String>,
#[arg(long)]
force: bool,
},
Compact {
collection: PathBuf,
#[arg(long)]
force: bool,
#[arg(long, default_value = "text")]
format: String,
},
Status {
collection: PathBuf,
#[arg(long, default_value = "text")]
format: String,
},
BenchmarkAdvanced {
collection: PathBuf,
#[arg(long)]
source: PathBuf,
#[arg(long, default_value = "text")]
format: String,
},
}
fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
Commands::Init { collection } => {
println!("Initializing SiftDB collection at: {}", collection.display());
let _db = SiftDB::init(&collection)?;
println!("✓ Collection initialized successfully");
}
Commands::Import {
collection,
from,
include,
exclude,
max_file_bytes,
} => {
println!("Importing files into: {}", collection.display());
println!("From source: {}", from.display());
let _db = SiftDB::open(&collection)?;
let mut options = IngestOptions::default();
options.max_file_bytes = max_file_bytes;
if !include.is_empty() {
options.include_patterns = include;
}
if !exclude.is_empty() {
options.exclude_patterns.extend(exclude);
}
let mut ingester = Ingester::new(collection, options);
let stats = match ingester.ingest_from_fs(&from) {
Ok(stats) => stats,
Err(e) => {
eprintln!("Import failed: {}", e);
std::process::exit(1);
}
};
println!("✓ Import completed:");
println!(" Files ingested: {}", stats.ingested);
println!(" Files skipped: {}", stats.skipped);
println!(" Errors: {}", stats.errors);
}
Commands::Find { collection, query, limit, path_glob, format } => {
let db = SiftDB::open(&collection)?;
let mut snapshot = db.snapshot()?;
let hits = snapshot.find(&query, path_glob.as_deref(), limit)?;
match format {
OutputFormat::Text => {
if hits.is_empty() {
println!("No matches found for: {}", query);
} else {
println!("Found {} matches for: {}", hits.len(), query);
for hit in hits {
println!("{}:{}: {}", hit.path, hit.line, hit.text);
}
}
}
OutputFormat::Json => {
let json = serde_json::to_string_pretty(&hits)?;
println!("{}", json);
}
}
}
Commands::Regex { collection, pattern, limit, path_glob, format } => {
println!("🚀 Milestone 0.2: Regex search");
let db = SiftDB::open(&collection)?;
let snapshot = db.snapshot()?;
let hits = snapshot.regex_find(&pattern, path_glob.as_deref(), limit)?;
match format {
OutputFormat::Text => {
if hits.is_empty() {
println!("No matches found for regex: {}", pattern);
} else {
println!("Found {} regex matches for: {}", hits.len(), pattern);
for hit in hits {
println!("{}:{}: {}", hit.path, hit.line, hit.text);
}
}
}
OutputFormat::Json => {
let json = serde_json::to_string_pretty(&hits)?;
println!("{}", json);
}
}
}
Commands::Open {
collection,
file,
start_line,
mut end_line,
} => {
let db = SiftDB::open(&collection)?;
let snapshot = db.snapshot()?;
if end_line == 0 {
end_line = u32::MAX; }
match snapshot.open_span(&file, start_line, end_line) {
Ok(span) => {
println!("=== {} (lines {}-{}) ===", span.path, span.start_line, span.end_line);
println!("{}", span.content);
}
Err(e) => {
eprintln!("Error opening file: {}", e);
std::process::exit(1);
}
}
}
Commands::Benchmark { collection, source, format } => {
match format.as_str() {
"text" => {
println!("Running SiftDB benchmark...");
println!("Collection: {}", collection.display());
println!("Source: {}", source.display());
let mut benchmark = SiftDBBenchmark::new(&collection, &source);
let _results = benchmark.run_all();
}
"json" => {
let mut benchmark = SiftDBBenchmark::new(&collection, &source);
let results = benchmark.run_all_quiet();
let json_output = serde_json::to_string_pretty(&results).unwrap();
print!("{}", json_output);
}
_ => {
eprintln!("Unknown format: {}. Use 'text' or 'json'", format);
std::process::exit(1);
}
}
}
Commands::Compare { path, format } => {
println!("Analyzing benchmark performance regressions...");
println!("Regression analysis not yet implemented");
}
Commands::Update { collection, from, include, exclude, force } => {
println!("🔄 Incremental update for: {}", collection.display());
let db = SiftDB::open(&collection)?;
if !db.has_incremental_cache() && !force {
println!("❌ No incremental cache found. Use full import first, or --force to create initial cache.");
std::process::exit(1);
}
match db.incremental_update(&from, &include, &exclude) {
Ok(delta_manifest) => {
println!("✅ Incremental update completed!");
println!(" Base epoch: {}", delta_manifest.base_epoch);
println!(" New epoch: {}", delta_manifest.delta_epoch);
println!(" Changes applied: {}", delta_manifest.changes.len());
}
Err(e) => {
if e.to_string().contains("No changes detected") {
println!("✅ Collection is up to date - no changes detected");
} else {
eprintln!("❌ Incremental update failed: {}", e);
std::process::exit(1);
}
}
}
}
Commands::Compact { collection, force, format } => {
use siftdb_core::compaction::CollectionCompactor;
println!("🧹 Collection compaction for: {}", collection.display());
let compactor = CollectionCompactor::new(&collection);
if !force && !compactor.needs_compaction()? {
println!("ℹ️ Collection does not need compaction. Use --force to compact anyway.");
return Ok(());
}
match compactor.compact() {
Ok(stats) => {
match format.as_str() {
"json" => {
let json = serde_json::to_string_pretty(&stats)?;
println!("{}", json);
}
_ => {
println!("✅ Compaction completed!");
println!(" Duration: {}s", stats.duration_secs);
println!(" Tombstones removed: {}", stats.tombstones_removed);
println!(" Segments compacted: {}", stats.segments_compacted);
println!(" Space reclaimed: {} bytes", stats.space_reclaimed_bytes);
println!(" Epoch: {} -> {}", stats.before_epoch, stats.after_epoch);
}
}
}
Err(e) => {
eprintln!("❌ Compaction failed: {}", e);
std::process::exit(1);
}
}
}
Commands::Status { collection, format } => {
let manager = siftdb_core::compaction::CompactionManager::new(&collection);
let status = manager.status()?;
match format.as_str() {
"json" => {
println!("{{");
println!(" \"needs_compaction\": {},", status.needs_compaction);
println!(" \"total_tombstones\": {},", status.total_tombstones);
println!(" \"oldest_tombstone_epoch\": {},", status.oldest_tombstone_epoch);
println!(" \"newest_tombstone_epoch\": {},", status.newest_tombstone_epoch);
if let Some(last_compaction) = status.last_compaction_at {
println!(" \"last_compaction_at\": {},", last_compaction);
} else {
println!(" \"last_compaction_at\": null,");
}
println!(" \"compaction_count\": {}", status.compaction_count);
println!("}}");
}
_ => {
println!("📊 Collection Status: {}", collection.display());
println!(" Needs compaction: {}", if status.needs_compaction { "Yes" } else { "No" });
println!(" Total tombstones: {}", status.total_tombstones);
println!(" Oldest tombstone epoch: {}", status.oldest_tombstone_epoch);
println!(" Newest tombstone epoch: {}", status.newest_tombstone_epoch);
if let Some(last_compaction) = status.last_compaction_at {
println!(" Last compaction: {}", last_compaction);
} else {
println!(" Last compaction: Never");
}
println!(" Total compactions: {}", status.compaction_count);
}
}
}
Commands::BenchmarkAdvanced { collection, source, format } => {
println!("🚀 SiftDB Advanced Features Benchmark");
println!("========================================");
println!("Collection: {}", collection.display());
println!("Source: {}", source.display());
println!();
let mut benchmark = siftdb_core::bench::SiftDBBenchmark::new(&collection, &source);
let results = benchmark.run_all_quiet();
match format.as_str() {
"text" => {
println!("📋 Basic Performance Results:");
for result in &results {
println!(" {}: {:.2}s", result.name, result.duration.as_secs_f64());
if let Some(qps) = result.queries_per_second {
println!(" Queries/sec: {:.1}", qps);
}
if let Some(throughput) = result.throughput_mbps {
println!(" Throughput: {:.1} MB/s", throughput);
}
}
println!("\n📋 Milestone 0.4 Features Status:");
println!(" ✅ Incremental Updates: Implemented");
println!(" ✅ Collection Compaction: Implemented");
println!(" ✅ Delta Manifests: Implemented");
println!(" ✅ File Timestamp Tracking: Implemented");
}
"json" => {
let json_output = serde_json::to_string_pretty(&results).unwrap();
print!("{}", json_output);
}
_ => {
eprintln!("Unknown format: {}. Use 'text' or 'json'", format);
std::process::exit(1);
}
}
}
}
Ok(())
}