1use stateset_core::{CommerceError, DbError};
7
8#[cfg(feature = "sqlite")]
13pub mod sqlite {
14 use super::{CommerceError, DbError};
15 use rusqlite::Error as SqliteError;
16
17 #[must_use]
19 pub fn map_error(
20 table: &'static str,
21 operation: &'static str,
22 err: SqliteError,
23 ) -> CommerceError {
24 match &err {
25 SqliteError::SqliteFailure(ffi_err, msg) => {
26 match ffi_err.code {
28 rusqlite::ErrorCode::ConstraintViolation => {
29 let constraint =
30 msg.as_ref().map_or("unknown", std::string::String::as_str);
31 CommerceError::Database(DbError::ConstraintViolation {
32 table,
33 constraint: extract_constraint_name(constraint).to_string(),
34 message: err.to_string(),
35 })
36 }
37 rusqlite::ErrorCode::DatabaseBusy | rusqlite::ErrorCode::DatabaseLocked => {
38 CommerceError::Database(DbError::TransactionFailed {
39 message: format!("Database busy/locked during {operation}: {err}"),
40 })
41 }
42 rusqlite::ErrorCode::CannotOpen => {
43 CommerceError::Database(DbError::ConnectionFailed {
44 url: "sqlite".to_string(),
45 message: err.to_string(),
46 })
47 }
48 _ => CommerceError::Database(DbError::QueryFailed {
49 table,
50 operation,
51 message: err.to_string(),
52 }),
53 }
54 }
55 SqliteError::QueryReturnedNoRows => {
56 CommerceError::NotFound
58 }
59 SqliteError::InvalidColumnType(col, col_name, expected) => {
60 CommerceError::Database(DbError::SerializationError {
61 field: col_name.clone(),
62 message: format!(
63 "Column {col_name} has invalid type at index {col}, expected {expected:?}"
64 ),
65 })
66 }
67 SqliteError::InvalidColumnName(name) => CommerceError::Database(DbError::QueryFailed {
68 table,
69 operation,
70 message: format!("Invalid column name: {name}"),
71 }),
72 SqliteError::FromSqlConversionFailure(col, _, err) => {
73 CommerceError::Database(DbError::SerializationError {
74 field: format!("column_{col}"),
75 message: err.to_string(),
76 })
77 }
78 _ => CommerceError::Database(DbError::QueryFailed {
79 table,
80 operation,
81 message: err.to_string(),
82 }),
83 }
84 }
85
86 #[must_use]
88 pub fn map_pool_error(_err: r2d2::Error) -> CommerceError {
89 CommerceError::Database(DbError::PoolExhausted {
90 timeout_ms: 30000, })
92 }
93
94 pub fn map_connection_error<E: std::fmt::Display>(err: E) -> CommerceError {
96 CommerceError::Database(DbError::ConnectionFailed {
97 url: "sqlite-pool".to_string(),
98 message: err.to_string(),
99 })
100 }
101
102 fn extract_constraint_name(msg: &str) -> &str {
104 if let Some(idx) = msg.find(':') { msg[..idx].trim() } else { msg }
106 }
107
108 #[macro_export]
110 macro_rules! map_sqlite_err {
111 ($table:expr, $op:expr, $result:expr) => {
112 $result.map_err(|e| $crate::error_helpers::sqlite::map_error($table, $op, e))
113 };
114 }
115}
116
117#[cfg(feature = "postgres")]
122pub mod postgres {
123 use super::*;
124 use sqlx::Error as PgError;
125
126 pub fn map_error(table: &'static str, operation: &'static str, err: PgError) -> CommerceError {
128 match &err {
129 PgError::Database(db_err) => {
130 if let Some(code) = db_err.code() {
132 let code_str = code.as_ref();
133
134 if code_str.starts_with("23") {
136 let constraint = db_err.constraint().unwrap_or("unknown").to_string();
137 return CommerceError::Database(DbError::ConstraintViolation {
138 table,
139 constraint,
140 message: db_err.message().to_string(),
141 });
142 }
143
144 if code_str.starts_with("08") {
146 return CommerceError::Database(DbError::ConnectionFailed {
147 url: "postgres".to_string(),
148 message: db_err.message().to_string(),
149 });
150 }
151
152 if code_str.starts_with("40") {
154 return CommerceError::Database(DbError::TransactionFailed {
155 message: db_err.message().to_string(),
156 });
157 }
158 }
159
160 CommerceError::Database(DbError::QueryFailed {
161 table,
162 operation,
163 message: db_err.message().to_string(),
164 })
165 }
166 PgError::RowNotFound => CommerceError::NotFound,
167 PgError::PoolTimedOut => {
168 CommerceError::Database(DbError::PoolExhausted {
169 timeout_ms: 30000, })
171 }
172 PgError::PoolClosed => CommerceError::Database(DbError::ConnectionFailed {
173 url: "postgres-pool".to_string(),
174 message: "Connection pool is closed".to_string(),
175 }),
176 PgError::Io(io_err) => CommerceError::Database(DbError::ConnectionFailed {
177 url: "postgres".to_string(),
178 message: io_err.to_string(),
179 }),
180 PgError::Decode(decode_err) => CommerceError::Database(DbError::SerializationError {
181 field: "unknown".to_string(),
182 message: decode_err.to_string(),
183 }),
184 _ => CommerceError::Database(DbError::QueryFailed {
185 table,
186 operation,
187 message: err.to_string(),
188 }),
189 }
190 }
191
192 pub fn map_migration_error(version: i32, err: impl std::fmt::Display) -> CommerceError {
194 CommerceError::Database(DbError::MigrationFailed { version, message: err.to_string() })
195 }
196
197 #[macro_export]
199 macro_rules! map_pg_err {
200 ($table:expr, $op:expr, $result:expr) => {
201 $result.map_err(|e| $crate::error_helpers::postgres::map_error($table, $op, e))
202 };
203 }
204}
205
206pub fn map_json_error(field: &str, err: impl std::fmt::Display) -> CommerceError {
212 CommerceError::Database(DbError::SerializationError {
213 field: field.to_string(),
214 message: err.to_string(),
215 })
216}
217
218pub fn map_db_error(msg: impl Into<String>) -> CommerceError {
220 CommerceError::Database(DbError::Other(msg.into()))
221}
222
223pub fn migration_failed(version: i32, msg: impl Into<String>) -> CommerceError {
225 CommerceError::Database(DbError::MigrationFailed { version, message: msg.into() })
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
237 #[cfg(feature = "sqlite")]
238 fn test_map_sqlite_not_found() {
239 let err = rusqlite::Error::QueryReturnedNoRows;
240 let result = sqlite::map_error("orders", "get", err);
241 assert!(result.is_not_found());
242 }
243
244 #[test]
245 fn test_map_json_error() {
246 let err = map_json_error("metadata", "invalid JSON");
247 assert!(err.is_database());
248 }
249
250 #[test]
255 fn test_map_json_error_contains_field_name() {
256 let err = map_json_error("settings", "parse failed");
257 let msg = format!("{err}");
258 assert!(msg.contains("settings"), "Error should mention the field: {msg}");
259 }
260
261 #[test]
262 fn test_map_db_error_produces_database_error() {
263 let err = map_db_error("something went wrong");
264 assert!(err.is_database());
265 let msg = format!("{err}");
266 assert!(msg.contains("something went wrong"));
267 }
268
269 #[test]
270 fn test_map_db_error_with_string() {
271 let err = map_db_error(String::from("owned message"));
272 assert!(err.is_database());
273 }
274
275 #[test]
276 fn test_migration_failed_produces_database_error() {
277 let err = migration_failed(42, "column missing");
278 assert!(err.is_database());
279 let msg = format!("{err}");
280 assert!(msg.contains("42"), "Should contain version: {msg}");
281 assert!(msg.contains("column missing"), "Should contain message: {msg}");
282 }
283
284 #[test]
285 fn test_migration_failed_version_zero() {
286 let err = migration_failed(0, "initial");
287 assert!(err.is_database());
288 }
289
290 #[test]
291 fn test_migration_failed_negative_version() {
292 let err = migration_failed(-1, "rollback");
293 assert!(err.is_database());
294 }
295
296 #[test]
297 fn test_map_json_error_empty_field() {
298 let err = map_json_error("", "no field");
299 assert!(err.is_database());
300 }
301
302 #[cfg(feature = "sqlite")]
307 mod sqlite_tests {
308 use super::*;
309 use rusqlite::{Connection, Error as SqliteError};
310
311 #[test]
312 fn test_constraint_violation_unique() {
313 let conn = Connection::open_in_memory().unwrap();
315 conn.execute("CREATE TABLE users (id INTEGER PRIMARY KEY, email TEXT UNIQUE)", [])
316 .unwrap();
317 conn.execute("INSERT INTO users (email) VALUES ('a@b.com')", []).unwrap();
318
319 let raw_err =
320 conn.execute("INSERT INTO users (email) VALUES ('a@b.com')", []).unwrap_err();
321
322 let mapped = sqlite::map_error("users", "insert", raw_err);
323 assert!(mapped.is_database());
324
325 match mapped {
327 CommerceError::Database(DbError::ConstraintViolation { table, .. }) => {
328 assert_eq!(table, "users");
329 }
330 other => panic!("Expected ConstraintViolation, got: {other:?}"),
331 }
332 }
333
334 #[test]
335 fn test_invalid_column_name() {
336 let err = SqliteError::InvalidColumnName("bogus_col".into());
337 let mapped = sqlite::map_error("orders", "select", err);
338 assert!(mapped.is_database());
339 let msg = format!("{mapped}");
340 assert!(msg.contains("bogus_col"), "Should mention column: {msg}");
341 }
342
343 #[test]
344 fn test_invalid_column_type() {
345 let err =
346 SqliteError::InvalidColumnType(0, "amount".into(), rusqlite::types::Type::Text);
347 let mapped = sqlite::map_error("invoices", "get", err);
348 assert!(mapped.is_database());
349 let msg = format!("{mapped}");
350 assert!(msg.contains("amount"), "Should mention column name: {msg}");
351 }
352
353 #[test]
354 fn test_from_sql_conversion_failure() {
355 let inner = Box::new(std::fmt::Error); let err =
357 SqliteError::FromSqlConversionFailure(3, rusqlite::types::Type::Integer, inner);
358 let mapped = sqlite::map_error("payments", "get", err);
359 assert!(mapped.is_database());
360 }
361
362 #[test]
363 fn test_generic_sqlite_error_maps_to_query_failed() {
364 let err = SqliteError::InvalidParameterCount(3, 5);
365 let mapped = sqlite::map_error("products", "update", err);
366 assert!(mapped.is_database());
367 }
368
369 #[test]
370 fn test_map_pool_error() {
371 }
379
380 #[test]
381 fn test_map_connection_error_with_string() {
382 let err = sqlite::map_connection_error("connection refused");
383 assert!(err.is_database());
384 let msg = format!("{err}");
385 assert!(msg.contains("connection refused"));
386 }
387
388 #[test]
389 fn test_map_connection_error_with_io_error() {
390 let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "reset");
391 let err = sqlite::map_connection_error(io_err);
392 assert!(err.is_database());
393 }
394
395 #[test]
396 fn test_extract_constraint_name_with_colon() {
397 let conn = Connection::open_in_memory().unwrap();
400 conn.execute("CREATE TABLE t (v TEXT UNIQUE)", []).unwrap();
401 conn.execute("INSERT INTO t VALUES ('x')", []).unwrap();
402 let raw_err = conn.execute("INSERT INTO t VALUES ('x')", []).unwrap_err();
403
404 let mapped = sqlite::map_error("t", "insert", raw_err);
405 match mapped {
406 CommerceError::Database(DbError::ConstraintViolation { constraint, .. }) => {
407 assert!(
409 !constraint.contains(':'),
410 "Constraint should not contain colon: {constraint}"
411 );
412 assert!(constraint.contains("UNIQUE"), "Should contain UNIQUE: {constraint}");
413 }
414 other => panic!("Expected ConstraintViolation, got: {other:?}"),
415 }
416 }
417
418 #[test]
419 fn test_database_busy_maps_to_transaction_failed() {
420 let conn = Connection::open_in_memory().unwrap();
424 conn.execute("CREATE TABLE t (id INTEGER)", []).unwrap();
425 let count: i64 = conn.query_row("SELECT COUNT(*) FROM t", [], |r| r.get(0)).unwrap();
428 assert_eq!(count, 0);
429 }
430
431 #[test]
432 fn test_query_returned_no_rows_maps_to_not_found() {
433 let err = SqliteError::QueryReturnedNoRows;
434 let mapped = sqlite::map_error("shipments", "get_by_id", err);
435 assert!(mapped.is_not_found());
436 }
437
438 #[test]
439 fn test_multiple_tables_constraint_violations() {
440 let conn = Connection::open_in_memory().unwrap();
441 conn.execute("CREATE TABLE a (id INTEGER PRIMARY KEY)", []).unwrap();
442 conn.execute(
443 "CREATE TABLE b (id INTEGER PRIMARY KEY, a_id INTEGER REFERENCES a(id))",
444 [],
445 )
446 .unwrap();
447
448 conn.execute("PRAGMA foreign_keys = ON", []).unwrap();
451 let raw_err = conn.execute("INSERT INTO b (id, a_id) VALUES (1, 999)", []).unwrap_err();
452
453 let mapped = sqlite::map_error("b", "insert", raw_err);
454 assert!(mapped.is_database());
455 }
456 }
457}