#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(i32)]
#[allow(dead_code)] pub enum CliExitCode {
Success = 0,
InvalidCliArgs = 2,
SchemaError = 3,
DataDirError = 4,
QueryExecutionError = 5,
WriteError = 6,
}
impl CliExitCode {
pub fn as_i32(self) -> i32 {
self as i32
}
pub fn hint(&self) -> &'static str {
match self {
Self::Success => "",
Self::InvalidCliArgs => "Run 'cqlite --help' for usage information",
Self::SchemaError => "Use ':schema load <file>' or '--schema <path>' to provide schema",
Self::DataDirError => {
"Use ':config data-dir <path>' or '--data-dir <path>' to set data directory"
}
Self::QueryExecutionError => "Check query syntax and ensure required data is available",
Self::WriteError => {
"Use '--writable --write-dir <path>' to enable write operations. Check mutation JSON format."
}
}
}
}
pub fn classify_error(err: &anyhow::Error) -> CliExitCode {
if let Some(core_err) = err.downcast_ref::<cqlite_core::Error>() {
return match core_err {
cqlite_core::Error::Schema(_) | cqlite_core::Error::Table(_) => {
CliExitCode::SchemaError
}
cqlite_core::Error::Io(_)
| cqlite_core::Error::Storage(_)
| cqlite_core::Error::InvalidPath(_)
| cqlite_core::Error::NotFound(_)
| cqlite_core::Error::Index(_) => CliExitCode::DataDirError,
cqlite_core::Error::QueryExecution(_)
| cqlite_core::Error::CqlParse(_)
| cqlite_core::Error::UnsupportedQuery(_)
| cqlite_core::Error::Parse(_)
| cqlite_core::Error::InvalidInput(_) => CliExitCode::QueryExecutionError,
_ => CliExitCode::QueryExecutionError,
};
}
let err_chain = format!("{:#}", err);
let err_lower = err_chain.to_lowercase();
if err_lower.contains("missing required flag: --data-dir") {
return CliExitCode::DataDirError;
}
if err_lower.contains("invalid argument")
|| err_lower.contains("unexpected argument")
|| err_lower.contains("required argument")
|| err_chain.contains("clap::Error")
|| err_lower.contains("usage:")
{
return CliExitCode::InvalidCliArgs;
}
if err_lower.contains("schema")
|| err_lower.contains("failed to parse cql")
|| err_lower.contains("failed to parse json")
|| err_lower.contains("missing keyspace")
|| err_lower.contains("missing table")
|| err_lower.contains("invalid column")
{
return CliExitCode::SchemaError;
}
if err_lower.contains("data-dir")
|| err_lower.contains("data directory")
|| err_lower.contains("sstable")
|| err_lower.contains("discovery")
|| err_lower.contains("failed to open")
|| err_lower.contains("no such file or directory")
|| err_lower.contains("not found")
|| err_lower.contains("cannot read file")
{
return CliExitCode::DataDirError;
}
if err_lower.contains("write")
|| err_lower.contains("mutation")
|| err_lower.contains("memtable")
|| err_lower.contains("wal")
|| err_lower.contains("flush")
|| err_lower.contains("compaction")
|| err_lower.contains("export")
|| err_lower.contains("writeengine")
{
return CliExitCode::WriteError;
}
CliExitCode::QueryExecutionError
}
pub fn print_error(err: &anyhow::Error, exit_code: CliExitCode) {
eprintln!("Error: {:#}", err);
let hint = get_error_hint(err, exit_code);
if !hint.is_empty() {
eprintln!("\nHint: {}", hint);
}
}
pub fn get_error_hint(err: &anyhow::Error, exit_code: CliExitCode) -> String {
if matches!(exit_code, CliExitCode::QueryExecutionError) {
let err_text = format!("{:#}", err).to_lowercase();
if err_text.contains("unsupported query") || err_text.contains("not supported") {
return build_unsupported_query_hint();
}
}
exit_code.hint().to_string()
}
fn build_unsupported_query_hint() -> String {
let mut hint = String::new();
hint.push_str("Supported SELECT features in M2:\n");
hint.push_str(" • SELECT with WHERE on partition/primary key\n");
hint.push_str(" • LIMIT clause for result pagination\n");
hint.push_str(" • DESCRIBE/DESC for schema information\n");
hint.push_str(" • USE for keyspace switching\n\n");
hint.push_str("Examples:\n");
hint.push_str(" SELECT * FROM users WHERE id = ? LIMIT 10\n");
hint.push_str(" DESCRIBE TABLE keyspace.users\n");
hint.push_str(" USE my_keyspace\n\n");
hint.push_str("Not supported: JOIN, subqueries, advanced aggregations\n");
hint.push_str("See: cqlite-cli/CLI_USAGE_EXAMPLES.md");
hint
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::anyhow;
#[test]
fn test_classify_invalid_args() {
let err = anyhow!("Invalid argument: --foo");
assert_eq!(classify_error(&err), CliExitCode::InvalidCliArgs);
let err = anyhow!("Required argument missing");
assert_eq!(classify_error(&err), CliExitCode::InvalidCliArgs);
}
#[test]
fn test_classify_schema_error() {
let err = anyhow!("Failed to parse schema file");
assert_eq!(classify_error(&err), CliExitCode::SchemaError);
let err = anyhow!("Missing keyspace in schema");
assert_eq!(classify_error(&err), CliExitCode::SchemaError);
}
#[test]
fn test_classify_data_dir_error() {
let err = anyhow!("Data directory not found");
assert_eq!(classify_error(&err), CliExitCode::DataDirError);
let err = anyhow!("Failed to open SSTable");
assert_eq!(classify_error(&err), CliExitCode::DataDirError);
}
#[test]
fn test_classify_missing_data_dir_flag() {
let err = anyhow!(
"Missing required flag: --data-dir\n\n\
One-shot query execution requires both --schema and --data-dir."
);
assert_eq!(classify_error(&err), CliExitCode::DataDirError);
assert_eq!(classify_error(&err).as_i32(), 4);
}
#[test]
fn test_classify_query_error() {
let err = anyhow!("Query execution failed");
assert_eq!(classify_error(&err), CliExitCode::QueryExecutionError);
let err = anyhow!("Syntax error in SELECT statement");
assert_eq!(classify_error(&err), CliExitCode::QueryExecutionError);
}
#[test]
fn test_exit_code_hints() {
assert_eq!(
CliExitCode::InvalidCliArgs.hint(),
"Run 'cqlite --help' for usage information"
);
assert_eq!(
CliExitCode::SchemaError.hint(),
"Use ':schema load <file>' or '--schema <path>' to provide schema"
);
assert_eq!(
CliExitCode::DataDirError.hint(),
"Use ':config data-dir <path>' or '--data-dir <path>' to set data directory"
);
assert_eq!(
CliExitCode::QueryExecutionError.hint(),
"Check query syntax and ensure required data is available"
);
assert_eq!(CliExitCode::Success.hint(), "");
}
#[test]
fn test_exit_code_as_i32() {
assert_eq!(CliExitCode::Success.as_i32(), 0);
assert_eq!(CliExitCode::InvalidCliArgs.as_i32(), 2);
assert_eq!(CliExitCode::SchemaError.as_i32(), 3);
assert_eq!(CliExitCode::DataDirError.as_i32(), 4);
assert_eq!(CliExitCode::QueryExecutionError.as_i32(), 5);
}
#[test]
fn test_unsupported_query_detection() {
let err = anyhow!("Unsupported query: JOIN operations not supported");
let exit_code = classify_error(&err);
assert_eq!(exit_code, CliExitCode::QueryExecutionError);
let hint = get_error_hint(&err, exit_code);
assert!(hint.contains("Supported SELECT features"));
assert!(hint.contains("WHERE on partition/primary key"));
assert!(hint.contains("LIMIT"));
assert!(hint.contains("DESCRIBE"));
}
#[test]
fn test_unsupported_query_hint_format() {
let err = anyhow!("Query feature not supported");
let exit_code = CliExitCode::QueryExecutionError;
let hint = get_error_hint(&err, exit_code);
assert!(hint.contains("Supported SELECT features"));
assert!(hint.contains("Examples:"));
assert!(hint.contains("Not supported"));
assert!(hint.contains("CLI_USAGE_EXAMPLES.md"));
}
#[test]
fn test_regular_query_error_hint() {
let err = anyhow!("Query execution failed: timeout");
let exit_code = classify_error(&err);
let hint = get_error_hint(&err, exit_code);
assert_eq!(
hint,
"Check query syntax and ensure required data is available"
);
}
#[test]
fn test_exit_code_5_for_unsupported_queries() {
let err = anyhow!("Unsupported query: subquery not allowed");
let exit_code = classify_error(&err);
assert_eq!(exit_code.as_i32(), 5);
}
#[test]
fn test_classify_core_schema_errors() {
let core_err = cqlite_core::Error::schema("Invalid schema definition");
let anyhow_err = anyhow::Error::new(core_err);
assert_eq!(classify_error(&anyhow_err), CliExitCode::SchemaError);
assert_eq!(classify_error(&anyhow_err).as_i32(), 3);
let table_err = cqlite_core::Error::Table("Table not found".to_string());
let anyhow_err = anyhow::Error::new(table_err);
assert_eq!(classify_error(&anyhow_err), CliExitCode::SchemaError);
assert_eq!(classify_error(&anyhow_err).as_i32(), 3);
}
#[test]
fn test_classify_core_discovery_errors() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let core_err = cqlite_core::Error::from(io_err);
let anyhow_err = anyhow::Error::new(core_err);
assert_eq!(classify_error(&anyhow_err), CliExitCode::DataDirError);
assert_eq!(classify_error(&anyhow_err).as_i32(), 4);
let storage_err = cqlite_core::Error::storage("SSTable not accessible");
let anyhow_err = anyhow::Error::new(storage_err);
assert_eq!(classify_error(&anyhow_err), CliExitCode::DataDirError);
assert_eq!(classify_error(&anyhow_err).as_i32(), 4);
let path_err = cqlite_core::Error::invalid_path("/nonexistent/path");
let anyhow_err = anyhow::Error::new(path_err);
assert_eq!(classify_error(&anyhow_err), CliExitCode::DataDirError);
assert_eq!(classify_error(&anyhow_err).as_i32(), 4);
let not_found_err = cqlite_core::Error::not_found("Resource not found");
let anyhow_err = anyhow::Error::new(not_found_err);
assert_eq!(classify_error(&anyhow_err), CliExitCode::DataDirError);
assert_eq!(classify_error(&anyhow_err).as_i32(), 4);
let index_err = cqlite_core::Error::index("Index read failure");
let anyhow_err = anyhow::Error::new(index_err);
assert_eq!(classify_error(&anyhow_err), CliExitCode::DataDirError);
assert_eq!(classify_error(&anyhow_err).as_i32(), 4);
}
#[test]
fn test_classify_core_query_errors() {
let query_err = cqlite_core::Error::query_execution("Query failed");
let anyhow_err = anyhow::Error::new(query_err);
assert_eq!(
classify_error(&anyhow_err),
CliExitCode::QueryExecutionError
);
assert_eq!(classify_error(&anyhow_err).as_i32(), 5);
let parse_err = cqlite_core::Error::cql_parse("Invalid SELECT syntax");
let anyhow_err = anyhow::Error::new(parse_err);
assert_eq!(
classify_error(&anyhow_err),
CliExitCode::QueryExecutionError
);
assert_eq!(classify_error(&anyhow_err).as_i32(), 5);
let unsupported_err = cqlite_core::Error::unsupported_query("JOIN not supported");
let anyhow_err = anyhow::Error::new(unsupported_err);
assert_eq!(
classify_error(&anyhow_err),
CliExitCode::QueryExecutionError
);
assert_eq!(classify_error(&anyhow_err).as_i32(), 5);
let parse_err = cqlite_core::Error::parse("Parse failure");
let anyhow_err = anyhow::Error::new(parse_err);
assert_eq!(
classify_error(&anyhow_err),
CliExitCode::QueryExecutionError
);
assert_eq!(classify_error(&anyhow_err).as_i32(), 5);
let invalid_input_err = cqlite_core::Error::invalid_input("Invalid query input");
let anyhow_err = anyhow::Error::new(invalid_input_err);
assert_eq!(
classify_error(&anyhow_err),
CliExitCode::QueryExecutionError
);
assert_eq!(classify_error(&anyhow_err).as_i32(), 5);
}
#[test]
fn test_classify_core_other_errors_default() {
let corruption_err = cqlite_core::Error::corruption("Data corrupted");
let anyhow_err = anyhow::Error::new(corruption_err);
assert_eq!(
classify_error(&anyhow_err),
CliExitCode::QueryExecutionError
);
assert_eq!(classify_error(&anyhow_err).as_i32(), 5);
}
}