use clap::{Parser, Subcommand};
use ddb_core::{
config::{Config, Table, TableCatalog},
engine::QueryExecutor,
lexer::Tokenizer,
output::{format_results, OutputFormat},
parser::Parser as SqlParser,
parser::Statement,
VERSION,
};
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "ddb")]
#[command(author, version, about = "A serviceless SQL interface for flat files", long_about = None)]
struct Cli {
#[arg(short, long)]
query: Option<String>,
#[arg(short = 'f', long)]
file: Option<PathBuf>,
#[arg(short = 'd', long, default_value = ",")]
delimiter: String,
#[arg(short = 'c', long)]
config: Option<PathBuf>,
#[arg(short, long, default_value = "table")]
output: String,
#[arg(long)]
debug: bool,
#[cfg(feature = "mcp")]
#[arg(long)]
mcp: bool,
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand)]
enum Commands {
Interactive,
Version,
}
fn main() {
let cli = Cli::parse();
#[cfg(feature = "mcp")]
if cli.mcp {
start_mcp_server();
return;
}
env_logger::init();
match &cli.command {
Some(Commands::Version) => {
println!("DDB v{}", VERSION);
return;
}
Some(Commands::Interactive) => {
println!("Interactive mode not yet implemented");
std::process::exit(1);
}
None => {
if let Some(query) = &cli.query {
let config = Config::load().unwrap_or_else(|e| {
eprintln!("Warning: Failed to load config: {}", e);
eprintln!("Using default configuration");
Config::default()
});
if cli.debug {
println!("=== Configuration ===");
println!("Database: {}", config.default_database);
println!("Schema Dir: {:?}", config.schema_dir);
println!("Delimiter: {:?}", config.default_delimiter);
println!();
}
let catalog = TableCatalog::load_from_config(&config).unwrap_or_else(|e| {
eprintln!("Warning: Failed to load table catalog: {}", e);
TableCatalog::new()
});
if cli.debug && !catalog.list_tables().is_empty() {
println!("=== Loaded Tables ===");
for table in catalog.list_tables() {
println!(" - {}", table);
}
println!();
}
if let Err(e) = execute_query(&cli, query, &config, &catalog) {
eprintln!("Error: {}", e);
std::process::exit(1);
}
} else {
eprintln!("Error: No query provided. Use --query or run 'ddb version'");
std::process::exit(1);
}
}
}
}
fn execute_query(cli: &Cli, query: &str, config: &Config, catalog: &TableCatalog) -> Result<(), Box<dyn std::error::Error>> {
let mut tokenizer = Tokenizer::new();
let tokens = tokenizer.tokenize(query)?;
if cli.debug {
println!("=== Tokens ===");
for token in &tokens {
println!(" {:?}", token);
}
println!();
}
let mut parser = SqlParser::new(tokens);
let statement = parser.parse()?;
if cli.debug {
println!("=== AST ===");
println!("{:#?}", statement);
println!();
}
match statement {
Statement::Select(select_stmt) => {
let table = if let Some(ref path) = cli.file {
let delimiter = cli
.delimiter
.chars()
.next()
.unwrap_or(config.default_delimiter);
Table {
name: "temp".to_string(),
database: "temp".to_string(),
data_file: path.to_string_lossy().to_string(),
columns: vec![],
field_delimiter: delimiter,
data_starts_on: config.data_starts_on,
comment_char: config.comment_char,
}
} else if let Some(ref table_name) = select_stmt.from {
catalog.get_table(table_name)
.ok_or_else(|| format!("Table '{}' not found in catalog. Use --file to specify a file, or add table definition to schema directory.", table_name))?
.clone()
} else {
return Err("SELECT requires a FROM clause or --file argument".into());
};
if cli.debug {
println!("=== Table Info ===");
println!("Name: {}", table.name);
println!("File: {}", table.data_file);
println!("Delimiter: {:?}", table.field_delimiter);
println!();
}
let executor = QueryExecutor::new();
let results = executor.execute_select(&select_stmt, &table)?;
if cli.debug {
println!("=== Results ===");
println!("{} rows returned", results.len());
println!();
}
let output_format = if !cli.output.is_empty() {
OutputFormat::from_str(&cli.output)
} else {
OutputFormat::from_str(&config.default_output_format)
};
let formatted = format_results(&results, output_format)?;
println!("{}", formatted);
Ok(())
}
Statement::Insert(insert_stmt) => {
let table = if let Some(ref path) = cli.file {
let delimiter = cli
.delimiter
.chars()
.next()
.unwrap_or(config.default_delimiter);
Table {
name: "temp".to_string(),
database: "temp".to_string(),
data_file: path.to_string_lossy().to_string(),
columns: vec![],
field_delimiter: delimiter,
data_starts_on: config.data_starts_on,
comment_char: config.comment_char,
}
} else {
catalog.get_table(&insert_stmt.table)
.ok_or_else(|| format!("Table '{}' not found in catalog. Use --file to specify a file, or add table definition to schema directory.", insert_stmt.table))?
.clone()
};
if cli.debug {
println!("=== Table Info ===");
println!("Name: {}", table.name);
println!("File: {}", table.data_file);
println!("Delimiter: {:?}", table.field_delimiter);
println!();
}
let executor = QueryExecutor::new();
let rows_inserted = executor.execute_insert(&insert_stmt, &table)?;
if cli.debug {
println!("=== Results ===");
println!("{} rows inserted", rows_inserted);
println!();
} else {
println!("{} rows inserted", rows_inserted);
}
Ok(())
}
Statement::Update(update_stmt) => {
let table = if let Some(ref path) = cli.file {
let delimiter = cli
.delimiter
.chars()
.next()
.unwrap_or(config.default_delimiter);
Table {
name: "temp".to_string(),
database: "temp".to_string(),
data_file: path.to_string_lossy().to_string(),
columns: vec![],
field_delimiter: delimiter,
data_starts_on: config.data_starts_on,
comment_char: config.comment_char,
}
} else {
catalog.get_table(&update_stmt.table)
.ok_or_else(|| format!("Table '{}' not found in catalog. Use --file to specify a file, or add table definition to schema directory.", update_stmt.table))?
.clone()
};
if cli.debug {
println!("=== Table Info ===");
println!("Name: {}", table.name);
println!("File: {}", table.data_file);
println!("Delimiter: {:?}", table.field_delimiter);
println!();
}
let executor = QueryExecutor::new();
let rows_updated = executor.execute_update(&update_stmt, &table)?;
if cli.debug {
println!("=== Results ===");
println!("{} rows updated", rows_updated);
println!();
} else {
println!("{} rows updated", rows_updated);
}
Ok(())
}
Statement::Upsert(upsert_stmt) => {
let table = if let Some(ref path) = cli.file {
let delimiter = cli
.delimiter
.chars()
.next()
.unwrap_or(config.default_delimiter);
Table {
name: "temp".to_string(),
database: "temp".to_string(),
data_file: path.to_string_lossy().to_string(),
columns: vec![],
field_delimiter: delimiter,
data_starts_on: config.data_starts_on,
comment_char: config.comment_char,
}
} else {
catalog.get_table(&upsert_stmt.table)
.ok_or_else(|| format!("Table '{}' not found in catalog. Use --file to specify a file, or add table definition to schema directory.", upsert_stmt.table))?
.clone()
};
if cli.debug {
println!("=== Table Info ===");
println!("Name: {}", table.name);
println!("File: {}", table.data_file);
println!("Delimiter: {:?}", table.field_delimiter);
println!();
}
let executor = QueryExecutor::new();
let (rows_inserted, rows_updated) = executor.execute_upsert(&upsert_stmt, &table)?;
if cli.debug {
println!("=== Results ===");
println!("{} rows inserted, {} rows updated", rows_inserted, rows_updated);
println!();
} else {
println!("{} rows inserted, {} rows updated", rows_inserted, rows_updated);
}
Ok(())
}
Statement::Delete(delete_stmt) => {
let table = if let Some(ref path) = cli.file {
let delimiter = cli
.delimiter
.chars()
.next()
.unwrap_or(config.default_delimiter);
Table {
name: "temp".to_string(),
database: "temp".to_string(),
data_file: path.to_string_lossy().to_string(),
columns: vec![],
field_delimiter: delimiter,
data_starts_on: config.data_starts_on,
comment_char: config.comment_char,
}
} else {
catalog.get_table(&delete_stmt.table)
.ok_or_else(|| format!("Table '{}' not found in catalog. Use --file to specify a file, or add table definition to schema directory.", delete_stmt.table))?
.clone()
};
if cli.debug {
println!("=== Table Info ===");
println!("Name: {}", table.name);
println!("File: {}", table.data_file);
println!("Delimiter: {:?}", table.field_delimiter);
println!();
}
let executor = QueryExecutor::new();
let rows_deleted = executor.execute_delete(&delete_stmt, &table)?;
if cli.debug {
println!("=== Results ===");
println!("{} rows deleted", rows_deleted);
println!();
} else {
println!("{} rows deleted", rows_deleted);
}
Ok(())
}
Statement::CreateTable(_) => {
Err("CREATE TABLE statements are not yet implemented".into())
}
Statement::DropTable(_) => {
Err("DROP TABLE statements are not yet implemented".into())
}
Statement::Use(_) => {
Err("USE statements are not yet implemented".into())
}
Statement::Show(_) => {
Err("SHOW statements are not yet implemented".into())
}
Statement::Set(_) => {
Err("SET statements are not yet implemented".into())
}
Statement::Describe(_) => {
Err("DESCRIBE statements are not yet implemented".into())
}
Statement::Begin | Statement::Commit | Statement::Rollback => {
Err("Transaction statements are not yet implemented".into())
}
}
}
#[cfg(feature = "mcp")]
fn start_mcp_server() {
use ddb_core::mcp::DdbMcpServer;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "ddb_mcp=info".into()),
)
.with(tracing_subscriber::fmt::layer().with_writer(std::io::stderr))
.init();
let runtime = tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime");
runtime.block_on(async {
let server = DdbMcpServer::new().expect("Failed to create MCP server");
eprintln!("DDB MCP Server v{} starting...", VERSION);
eprintln!("Server capabilities: tools, resources, prompts");
if let Err(e) = server.run().await {
eprintln!("MCP server error: {}", e);
std::process::exit(1);
}
});
}