1use thiserror::Error;
6
7use crate::batch::ExitCode;
8
9#[derive(Error, Debug)]
13pub enum CliError {
14 #[error("Database error: {0}")]
16 Database(#[from] alopex_embedded::Error),
17
18 #[error("IO error: {0}")]
20 Io(#[from] std::io::Error),
21
22 #[error("Invalid argument: {0}")]
24 InvalidArgument(String),
25
26 #[error("プロファイル '{0}' が見つかりません。alopex profile list で一覧を確認してください。")]
28 #[allow(dead_code)]
29 ProfileNotFound(String),
30
31 #[error(
33 "`--profile` と `--data-dir` は同時に指定できません。どちらか一方を使用してください。"
34 )]
35 #[allow(dead_code)]
36 ConflictingOptions,
37
38 #[error(
40 "トランザクション ID '{0}' は無効です。alopex kv txn begin で新しいトランザクションを開始してください。"
41 )]
42 #[allow(dead_code)]
43 InvalidTransactionId(String),
44
45 #[error(
47 "トランザクション '{0}' がタイムアウトしました(60秒)。自動的にロールバックされました。"
48 )]
49 #[allow(dead_code)]
50 TransactionTimeout(String),
51
52 #[error("Query timeout: {0}. Suggestion: Use --deadline 120s to increase timeout.")]
54 #[allow(dead_code)]
55 Timeout(String),
56
57 #[error("Query cancelled by user.")]
59 #[allow(dead_code)]
60 Cancelled,
61
62 #[error("SQL クエリを指定してください。引数、-f オプション、または標準入力から入力できます。")]
64 #[allow(dead_code)]
65 NoQueryProvided,
66
67 #[error("未知のインデックスタイプ: '{0}'。許可値: minmax, bloom")]
69 #[allow(dead_code)]
70 UnknownIndexType(String),
71
72 #[error("未知の圧縮形式: '{0}'。許可値: lz4, zstd, none")]
74 #[allow(dead_code)]
75 UnknownCompressionType(String),
76
77 #[error("ファイルフォーマット v{file} は CLI v{cli} でサポートされていません。CLI をアップグレードしてください。")]
79 #[allow(dead_code)]
80 IncompatibleVersion { cli: String, file: String },
81
82 #[error("S3 error: {0}")]
84 #[allow(dead_code)]
85 S3(String),
86
87 #[error("Credentials error: {0}")]
89 Credentials(String),
90
91 #[error("Parse error: {0}")]
93 #[allow(dead_code)]
94 Parse(String),
95
96 #[error("JSON error: {0}")]
98 Json(#[from] serde_json::Error),
99
100 #[error("Server connection error: {0}")]
102 #[allow(dead_code)]
103 ServerConnection(String),
104
105 #[error("Server does not support this command: {0}")]
107 #[allow(dead_code)]
108 ServerUnsupported(String),
109
110 #[error("Distributed read {outcome}: {reason}")]
113 DistributedReadOutcome {
114 outcome: String,
115 reason: String,
116 exit_code: ExitCode,
117 },
118
119 #[error("Cluster management {outcome}: {reason}")]
123 ClusterManagementOutcome {
124 outcome: String,
125 reason: String,
126 exit_code: ExitCode,
127 },
128}
129
130pub type Result<T> = std::result::Result<T, CliError>;
132
133pub fn handle_error(error: &CliError, verbose: bool) {
138 if verbose {
139 eprintln!("Error: {:?}", error); } else {
141 eprintln!("Error: {}", error); }
143}
144
145impl CliError {
150 pub fn exit_code(&self) -> ExitCode {
151 match self {
152 Self::ClusterManagementOutcome { exit_code, .. } => *exit_code,
153 Self::DistributedReadOutcome { exit_code, .. } => *exit_code,
154 _ => ExitCode::Error,
155 }
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn test_invalid_argument_error() {
165 let err = CliError::InvalidArgument("test message".to_string());
166 assert_eq!(format!("{}", err), "Invalid argument: test message");
167 }
168
169 #[test]
170 fn test_s3_error() {
171 let err = CliError::S3("bucket not found".to_string());
172 assert_eq!(format!("{}", err), "S3 error: bucket not found");
173 }
174
175 #[test]
176 fn test_credentials_error() {
177 let err = CliError::Credentials("AWS_ACCESS_KEY_ID not set".to_string());
178 assert_eq!(
179 format!("{}", err),
180 "Credentials error: AWS_ACCESS_KEY_ID not set"
181 );
182 }
183
184 #[test]
185 fn cluster_management_error_preserves_its_exit_class() {
186 let err = CliError::ClusterManagementOutcome {
187 outcome: "retryable_failure".to_string(),
188 reason: "not_leader".to_string(),
189 exit_code: ExitCode::Retryable,
190 };
191
192 assert_eq!(err.exit_code(), ExitCode::Retryable);
193 }
194
195 #[test]
196 fn distributed_read_error_preserves_its_exit_class() {
197 let err = CliError::DistributedReadOutcome {
198 outcome: "unsupported".to_string(),
199 reason: "remote DDL is outside the closed catalog".to_string(),
200 exit_code: ExitCode::Unsupported,
201 };
202 assert_eq!(err.exit_code(), ExitCode::Unsupported);
203 }
204
205 #[test]
206 fn test_parse_error() {
207 let err = CliError::Parse("invalid JSON".to_string());
208 assert_eq!(format!("{}", err), "Parse error: invalid JSON");
209 }
210
211 #[test]
212 fn test_io_error_from() {
213 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
214 let err: CliError = io_err.into();
215 assert!(matches!(err, CliError::Io(_)));
216 }
217}