1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16#[repr(i32)]
17#[allow(dead_code)] pub enum CliExitCode {
19 Success = 0,
21 InvalidCliArgs = 2,
23 SchemaError = 3,
25 DataDirError = 4,
27 QueryExecutionError = 5,
29 WriteError = 6,
31}
32
33impl CliExitCode {
34 pub fn as_i32(self) -> i32 {
36 self as i32
37 }
38
39 pub fn hint(&self) -> &'static str {
44 match self {
45 Self::Success => "",
46 Self::InvalidCliArgs => "Run 'cqlite --help' for usage information",
47 Self::SchemaError => "Use ':schema load <file>' or '--schema <path>' to provide schema",
48 Self::DataDirError => {
49 "Use ':config data-dir <path>' or '--data-dir <path>' to set data directory"
50 }
51 Self::QueryExecutionError => "Check query syntax and ensure required data is available",
52 Self::WriteError => {
53 "Use '--writable --write-dir <path>' to enable write operations. Check mutation JSON format."
54 }
55 }
56 }
57}
58
59pub fn classify_error(err: &anyhow::Error) -> CliExitCode {
83 if let Some(core_err) = err.downcast_ref::<cqlite_core::Error>() {
85 return match core_err {
86 cqlite_core::Error::Schema(_) | cqlite_core::Error::Table(_) => {
88 CliExitCode::SchemaError
89 }
90
91 cqlite_core::Error::Io(_)
93 | cqlite_core::Error::Storage(_)
94 | cqlite_core::Error::InvalidPath(_)
95 | cqlite_core::Error::NotFound(_)
96 | cqlite_core::Error::Index(_) => CliExitCode::DataDirError,
97
98 cqlite_core::Error::QueryExecution(_)
100 | cqlite_core::Error::CqlParse(_)
101 | cqlite_core::Error::UnsupportedQuery(_)
102 | cqlite_core::Error::Parse(_)
103 | cqlite_core::Error::InvalidInput(_) => CliExitCode::QueryExecutionError,
104
105 _ => CliExitCode::QueryExecutionError,
107 };
108 }
109
110 let err_chain = format!("{:#}", err);
112 let err_lower = err_chain.to_lowercase();
113
114 if err_lower.contains("missing required flag: --data-dir") {
119 return CliExitCode::DataDirError;
120 }
121
122 if err_lower.contains("invalid argument")
124 || err_lower.contains("unexpected argument")
125 || err_lower.contains("required argument")
126 || err_chain.contains("clap::Error")
127 || err_lower.contains("usage:")
128 {
129 return CliExitCode::InvalidCliArgs;
130 }
131
132 if err_lower.contains("schema")
134 || err_lower.contains("failed to parse cql")
135 || err_lower.contains("failed to parse json")
136 || err_lower.contains("missing keyspace")
137 || err_lower.contains("missing table")
138 || err_lower.contains("invalid column")
139 {
140 return CliExitCode::SchemaError;
141 }
142
143 if err_lower.contains("data-dir")
145 || err_lower.contains("data directory")
146 || err_lower.contains("sstable")
147 || err_lower.contains("discovery")
148 || err_lower.contains("failed to open")
149 || err_lower.contains("no such file or directory")
150 || err_lower.contains("not found")
151 || err_lower.contains("cannot read file")
152 {
153 return CliExitCode::DataDirError;
154 }
155
156 if err_lower.contains("write")
158 || err_lower.contains("mutation")
159 || err_lower.contains("memtable")
160 || err_lower.contains("wal")
161 || err_lower.contains("flush")
162 || err_lower.contains("compaction")
163 || err_lower.contains("export")
164 || err_lower.contains("writeengine")
165 {
166 return CliExitCode::WriteError;
167 }
168
169 CliExitCode::QueryExecutionError
171}
172
173pub fn print_error(err: &anyhow::Error, exit_code: CliExitCode) {
196 eprintln!("Error: {:#}", err);
197
198 let hint = get_error_hint(err, exit_code);
199 if !hint.is_empty() {
200 eprintln!("\nHint: {}", hint);
201 }
202}
203
204pub fn get_error_hint(err: &anyhow::Error, exit_code: CliExitCode) -> String {
221 if matches!(exit_code, CliExitCode::QueryExecutionError) {
223 let err_text = format!("{:#}", err).to_lowercase();
224
225 if err_text.contains("unsupported query") || err_text.contains("not supported") {
227 return build_unsupported_query_hint();
228 }
229 }
230
231 exit_code.hint().to_string()
233}
234
235fn build_unsupported_query_hint() -> String {
240 let mut hint = String::new();
241 hint.push_str("Supported SELECT features in M2:\n");
242 hint.push_str(" • SELECT with WHERE on partition/primary key\n");
243 hint.push_str(" • LIMIT clause for result pagination\n");
244 hint.push_str(" • DESCRIBE/DESC for schema information\n");
245 hint.push_str(" • USE for keyspace switching\n\n");
246 hint.push_str("Examples:\n");
247 hint.push_str(" SELECT * FROM users WHERE id = ? LIMIT 10\n");
248 hint.push_str(" DESCRIBE TABLE keyspace.users\n");
249 hint.push_str(" USE my_keyspace\n\n");
250 hint.push_str("Not supported: JOIN, subqueries, advanced aggregations\n");
251 hint.push_str("See: cqlite-cli/CLI_USAGE_EXAMPLES.md");
252 hint
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258 use anyhow::anyhow;
259
260 #[test]
261 fn test_classify_invalid_args() {
262 let err = anyhow!("Invalid argument: --foo");
263 assert_eq!(classify_error(&err), CliExitCode::InvalidCliArgs);
264
265 let err = anyhow!("Required argument missing");
266 assert_eq!(classify_error(&err), CliExitCode::InvalidCliArgs);
267 }
268
269 #[test]
270 fn test_classify_schema_error() {
271 let err = anyhow!("Failed to parse schema file");
272 assert_eq!(classify_error(&err), CliExitCode::SchemaError);
273
274 let err = anyhow!("Missing keyspace in schema");
275 assert_eq!(classify_error(&err), CliExitCode::SchemaError);
276 }
277
278 #[test]
279 fn test_classify_data_dir_error() {
280 let err = anyhow!("Data directory not found");
281 assert_eq!(classify_error(&err), CliExitCode::DataDirError);
282
283 let err = anyhow!("Failed to open SSTable");
284 assert_eq!(classify_error(&err), CliExitCode::DataDirError);
285 }
286
287 #[test]
288 fn test_classify_missing_data_dir_flag() {
289 let err = anyhow!(
291 "Missing required flag: --data-dir\n\n\
292 One-shot query execution requires both --schema and --data-dir."
293 );
294 assert_eq!(classify_error(&err), CliExitCode::DataDirError);
295 assert_eq!(classify_error(&err).as_i32(), 4);
296 }
297
298 #[test]
299 fn test_classify_query_error() {
300 let err = anyhow!("Query execution failed");
301 assert_eq!(classify_error(&err), CliExitCode::QueryExecutionError);
302
303 let err = anyhow!("Syntax error in SELECT statement");
304 assert_eq!(classify_error(&err), CliExitCode::QueryExecutionError);
305 }
306
307 #[test]
308 fn test_exit_code_hints() {
309 assert_eq!(
310 CliExitCode::InvalidCliArgs.hint(),
311 "Run 'cqlite --help' for usage information"
312 );
313 assert_eq!(
314 CliExitCode::SchemaError.hint(),
315 "Use ':schema load <file>' or '--schema <path>' to provide schema"
316 );
317 assert_eq!(
318 CliExitCode::DataDirError.hint(),
319 "Use ':config data-dir <path>' or '--data-dir <path>' to set data directory"
320 );
321 assert_eq!(
322 CliExitCode::QueryExecutionError.hint(),
323 "Check query syntax and ensure required data is available"
324 );
325 assert_eq!(CliExitCode::Success.hint(), "");
326 }
327
328 #[test]
329 fn test_exit_code_as_i32() {
330 assert_eq!(CliExitCode::Success.as_i32(), 0);
331 assert_eq!(CliExitCode::InvalidCliArgs.as_i32(), 2);
332 assert_eq!(CliExitCode::SchemaError.as_i32(), 3);
333 assert_eq!(CliExitCode::DataDirError.as_i32(), 4);
334 assert_eq!(CliExitCode::QueryExecutionError.as_i32(), 5);
335 }
336
337 #[test]
338 fn test_unsupported_query_detection() {
339 let err = anyhow!("Unsupported query: JOIN operations not supported");
340 let exit_code = classify_error(&err);
341 assert_eq!(exit_code, CliExitCode::QueryExecutionError);
342
343 let hint = get_error_hint(&err, exit_code);
344 assert!(hint.contains("Supported SELECT features"));
345 assert!(hint.contains("WHERE on partition/primary key"));
346 assert!(hint.contains("LIMIT"));
347 assert!(hint.contains("DESCRIBE"));
348 }
349
350 #[test]
351 fn test_unsupported_query_hint_format() {
352 let err = anyhow!("Query feature not supported");
353 let exit_code = CliExitCode::QueryExecutionError;
354 let hint = get_error_hint(&err, exit_code);
355
356 assert!(hint.contains("Supported SELECT features"));
358 assert!(hint.contains("Examples:"));
359 assert!(hint.contains("Not supported"));
360 assert!(hint.contains("CLI_USAGE_EXAMPLES.md"));
361 }
362
363 #[test]
364 fn test_regular_query_error_hint() {
365 let err = anyhow!("Query execution failed: timeout");
366 let exit_code = classify_error(&err);
367 let hint = get_error_hint(&err, exit_code);
368
369 assert_eq!(
371 hint,
372 "Check query syntax and ensure required data is available"
373 );
374 }
375
376 #[test]
377 fn test_exit_code_5_for_unsupported_queries() {
378 let err = anyhow!("Unsupported query: subquery not allowed");
379 let exit_code = classify_error(&err);
380 assert_eq!(exit_code.as_i32(), 5);
381 }
382
383 #[test]
384 fn test_classify_core_schema_errors() {
385 let core_err = cqlite_core::Error::schema("Invalid schema definition");
387 let anyhow_err = anyhow::Error::new(core_err);
388 assert_eq!(classify_error(&anyhow_err), CliExitCode::SchemaError);
389 assert_eq!(classify_error(&anyhow_err).as_i32(), 3);
390
391 let table_err = cqlite_core::Error::Table("Table not found".to_string());
393 let anyhow_err = anyhow::Error::new(table_err);
394 assert_eq!(classify_error(&anyhow_err), CliExitCode::SchemaError);
395 assert_eq!(classify_error(&anyhow_err).as_i32(), 3);
396 }
397
398 #[test]
399 fn test_classify_core_discovery_errors() {
400 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
402 let core_err = cqlite_core::Error::from(io_err);
403 let anyhow_err = anyhow::Error::new(core_err);
404 assert_eq!(classify_error(&anyhow_err), CliExitCode::DataDirError);
405 assert_eq!(classify_error(&anyhow_err).as_i32(), 4);
406
407 let storage_err = cqlite_core::Error::storage("SSTable not accessible");
409 let anyhow_err = anyhow::Error::new(storage_err);
410 assert_eq!(classify_error(&anyhow_err), CliExitCode::DataDirError);
411 assert_eq!(classify_error(&anyhow_err).as_i32(), 4);
412
413 let path_err = cqlite_core::Error::invalid_path("/nonexistent/path");
415 let anyhow_err = anyhow::Error::new(path_err);
416 assert_eq!(classify_error(&anyhow_err), CliExitCode::DataDirError);
417 assert_eq!(classify_error(&anyhow_err).as_i32(), 4);
418
419 let not_found_err = cqlite_core::Error::not_found("Resource not found");
421 let anyhow_err = anyhow::Error::new(not_found_err);
422 assert_eq!(classify_error(&anyhow_err), CliExitCode::DataDirError);
423 assert_eq!(classify_error(&anyhow_err).as_i32(), 4);
424
425 let index_err = cqlite_core::Error::index("Index read failure");
427 let anyhow_err = anyhow::Error::new(index_err);
428 assert_eq!(classify_error(&anyhow_err), CliExitCode::DataDirError);
429 assert_eq!(classify_error(&anyhow_err).as_i32(), 4);
430 }
431
432 #[test]
433 fn test_classify_core_query_errors() {
434 let query_err = cqlite_core::Error::query_execution("Query failed");
436 let anyhow_err = anyhow::Error::new(query_err);
437 assert_eq!(
438 classify_error(&anyhow_err),
439 CliExitCode::QueryExecutionError
440 );
441 assert_eq!(classify_error(&anyhow_err).as_i32(), 5);
442
443 let parse_err = cqlite_core::Error::cql_parse("Invalid SELECT syntax");
445 let anyhow_err = anyhow::Error::new(parse_err);
446 assert_eq!(
447 classify_error(&anyhow_err),
448 CliExitCode::QueryExecutionError
449 );
450 assert_eq!(classify_error(&anyhow_err).as_i32(), 5);
451
452 let unsupported_err = cqlite_core::Error::unsupported_query("JOIN not supported");
454 let anyhow_err = anyhow::Error::new(unsupported_err);
455 assert_eq!(
456 classify_error(&anyhow_err),
457 CliExitCode::QueryExecutionError
458 );
459 assert_eq!(classify_error(&anyhow_err).as_i32(), 5);
460
461 let parse_err = cqlite_core::Error::parse("Parse failure");
463 let anyhow_err = anyhow::Error::new(parse_err);
464 assert_eq!(
465 classify_error(&anyhow_err),
466 CliExitCode::QueryExecutionError
467 );
468 assert_eq!(classify_error(&anyhow_err).as_i32(), 5);
469
470 let invalid_input_err = cqlite_core::Error::invalid_input("Invalid query input");
472 let anyhow_err = anyhow::Error::new(invalid_input_err);
473 assert_eq!(
474 classify_error(&anyhow_err),
475 CliExitCode::QueryExecutionError
476 );
477 assert_eq!(classify_error(&anyhow_err).as_i32(), 5);
478 }
479
480 #[test]
481 fn test_classify_core_other_errors_default() {
482 let corruption_err = cqlite_core::Error::corruption("Data corrupted");
484 let anyhow_err = anyhow::Error::new(corruption_err);
485 assert_eq!(
486 classify_error(&anyhow_err),
487 CliExitCode::QueryExecutionError
488 );
489 assert_eq!(classify_error(&anyhow_err).as_i32(), 5);
490 }
491}