1#![deny(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
8#![cfg_attr(
9 not(test),
10 deny(
11 clippy::unwrap_used,
12 clippy::unreachable,
13 clippy::expect_used,
14 clippy::panic,
15 clippy::unimplemented,
16 clippy::todo
17 )
18)]
19
20#[cfg(feature = "audit-log")]
21pub mod audit_log;
22#[cfg(feature = "runtime-component")]
23mod component;
24pub mod connection;
25mod index;
26#[cfg(feature = "mail-outbox")]
27pub mod mail_outbox;
28pub mod pagination;
29pub mod retry;
30#[cfg(feature = "runtime-lease")]
31pub mod runtime_lease;
32#[cfg(feature = "scheduled-task")]
33pub mod scheduled_task;
34pub mod search_query;
35pub mod sort;
36#[cfg(feature = "system-config")]
37pub mod system_config;
38pub mod transaction;
39
40#[cfg(feature = "audit-log")]
41pub use audit_log::{
42 AUDIT_LOG_ACTION_COLUMN, AUDIT_LOG_ACTION_CREATED_ID_INDEX,
43 AUDIT_LOG_ACTION_CREATED_USER_INDEX, AUDIT_LOG_ACTION_INDEX, AUDIT_LOG_CREATED_AT_COLUMN,
44 AUDIT_LOG_CREATED_AT_INDEX, AUDIT_LOG_CREATED_ID_INDEX, AUDIT_LOG_DETAILS_COLUMN,
45 AUDIT_LOG_ENTITY_ID_COLUMN, AUDIT_LOG_ENTITY_NAME_COLUMN, AUDIT_LOG_ENTITY_TYPE_COLUMN,
46 AUDIT_LOG_ENTITY_TYPE_CREATED_ID_INDEX, AUDIT_LOG_ID_COLUMN, AUDIT_LOG_IP_ADDRESS_COLUMN,
47 AUDIT_LOG_USER_AGENT_COLUMN, AUDIT_LOG_USER_CREATED_ID_INDEX, AUDIT_LOG_USER_ID_COLUMN,
48 AUDIT_LOG_USER_ID_INDEX, AUDIT_LOGS_TABLE, AuditLogCreate, AuditLogCursorSlice,
49 AuditLogDbStore, AuditLogQuery, count_audit_logs_created_between,
50 count_audit_logs_created_between_with_actions,
51 count_distinct_audit_log_users_created_between_with_actions, create_audit_log_requests,
52 create_audit_log_row, create_audit_log_rows, create_audit_logs_action_created_id_index,
53 create_audit_logs_action_created_user_index, create_audit_logs_action_index,
54 create_audit_logs_base_indexes, create_audit_logs_created_at_index,
55 create_audit_logs_created_id_index, create_audit_logs_entity_type_created_id_index,
56 create_audit_logs_query_indexes, create_audit_logs_table,
57 create_audit_logs_user_created_id_index, create_audit_logs_user_id_index,
58 delete_audit_logs_before, drop_audit_logs_table, find_audit_logs_with_filters_cursor,
59};
60#[cfg(feature = "runtime-component")]
61pub use component::{
62 DATABASE_COMPONENT, DATABASE_CONNECTIONS_SHUTDOWN_PHASE, DATABASE_HEALTH_CHECK,
63 DATABASE_HEALTH_CHECK_TIMEOUT, DatabaseHealthComponent, DatabaseRuntimeComponent,
64 check_database_component, database_component, database_component_after,
65 database_health_component, database_health_options, ping_database,
66};
67pub use connection::{
68 DatabaseConfig, DbHandles, connect, connect_reader_for_writer,
69 connect_reader_for_writer_with_metrics, connect_with_metrics,
70};
71pub use index::{drop_index_if_exists, rename_mysql_index_if_exists};
72#[cfg(feature = "mail-outbox")]
73pub use mail_outbox::{
74 MAIL_OUTBOX_ATTEMPT_COUNT_COLUMN, MAIL_OUTBOX_CREATED_AT_COLUMN, MAIL_OUTBOX_DUE_INDEX,
75 MAIL_OUTBOX_ID_COLUMN, MAIL_OUTBOX_LAST_ERROR_COLUMN, MAIL_OUTBOX_NEXT_ATTEMPT_AT_COLUMN,
76 MAIL_OUTBOX_PAYLOAD_JSON_COLUMN, MAIL_OUTBOX_PROCESSING_INDEX,
77 MAIL_OUTBOX_PROCESSING_STARTED_AT_COLUMN, MAIL_OUTBOX_SENT_AT_COLUMN,
78 MAIL_OUTBOX_SENT_AT_INDEX, MAIL_OUTBOX_STATUS_COLUMN, MAIL_OUTBOX_TABLE,
79 MAIL_OUTBOX_TEMPLATE_CODE_COLUMN, MAIL_OUTBOX_TO_ADDRESS_COLUMN, MAIL_OUTBOX_TO_NAME_COLUMN,
80 MAIL_OUTBOX_UPDATED_AT_COLUMN, MailOutboxCreate, MailOutboxDbStore,
81 create_mail_outbox_due_index, create_mail_outbox_processing_index, create_mail_outbox_row,
82 create_mail_outbox_sent_at_index, create_mail_outbox_table, drop_mail_outbox_table,
83};
84#[cfg(feature = "runtime-lease")]
85pub use runtime_lease::{
86 RUNTIME_LEASE_CREATED_AT_COLUMN, RUNTIME_LEASE_EXPIRES_AT_COLUMN, RUNTIME_LEASE_ID_COLUMN,
87 RUNTIME_LEASE_LAST_RENEWED_AT_COLUMN, RUNTIME_LEASE_OWNER_ID_COLUMN,
88 RUNTIME_LEASE_UPDATED_AT_COLUMN, RUNTIME_LEASES_TABLE, RuntimeLeaseDbStore,
89 create_runtime_leases_table, drop_runtime_leases_table,
90};
91#[cfg(feature = "scheduled-task")]
92pub use scheduled_task::{
93 SCHEDULED_TASK_CLAIM_EXPIRES_AT_COLUMN, SCHEDULED_TASK_CLAIM_OWNER_ID_COLUMN,
94 SCHEDULED_TASK_CREATED_AT_COLUMN, SCHEDULED_TASK_DISPLAY_NAME_COLUMN, SCHEDULED_TASK_ID_COLUMN,
95 SCHEDULED_TASK_LAST_CLAIMED_AT_COLUMN, SCHEDULED_TASK_LAST_FINISHED_AT_COLUMN,
96 SCHEDULED_TASK_NAME_COLUMN, SCHEDULED_TASK_NAMESPACE_COLUMN,
97 SCHEDULED_TASK_NAMESPACE_NAME_UNIQUE_INDEX, SCHEDULED_TASK_NEXT_RUN_AT_COLUMN,
98 SCHEDULED_TASK_NEXT_RUN_INDEX, SCHEDULED_TASK_UPDATED_AT_COLUMN, SCHEDULED_TASKS_TABLE,
99 ScheduledTaskDbStore, create_scheduled_tasks_namespace_name_unique_index,
100 create_scheduled_tasks_next_run_index, create_scheduled_tasks_table,
101 drop_scheduled_tasks_table,
102};
103#[cfg(feature = "system-config")]
104pub use system_config::{
105 PresentedSystemConfig, SystemConfigCursorSlice, SystemConfigDbBinding, SystemConfigDbStore,
106 SystemConfigUpsert, present_system_config,
107};
108#[cfg(feature = "system-config")]
109pub use system_config::{
110 SYSTEM_CONFIG_CATEGORY_COLUMN, SYSTEM_CONFIG_DESCRIPTION_COLUMN, SYSTEM_CONFIG_ID_COLUMN,
111 SYSTEM_CONFIG_IS_SENSITIVE_COLUMN, SYSTEM_CONFIG_KEY_COLUMN, SYSTEM_CONFIG_KEY_UNIQUE_INDEX,
112 SYSTEM_CONFIG_NAMESPACE_COLUMN, SYSTEM_CONFIG_REQUIRES_RESTART_COLUMN,
113 SYSTEM_CONFIG_SOURCE_COLUMN, SYSTEM_CONFIG_TABLE, SYSTEM_CONFIG_UPDATED_AT_COLUMN,
114 SYSTEM_CONFIG_UPDATED_BY_COLUMN, SYSTEM_CONFIG_VALUE_COLUMN, SYSTEM_CONFIG_VALUE_TYPE_COLUMN,
115 SYSTEM_CONFIG_VISIBILITY_COLUMN, create_system_config_key_unique_index,
116 create_system_config_table, drop_system_config_table,
117};
118
119pub type Result<T> = std::result::Result<T, DbError>;
121
122#[derive(Clone, Copy, Debug, Eq, PartialEq)]
124pub enum DatabaseErrorKind {
125 Deadlock,
127 SerializationFailure,
129 LockTimeout,
132 UniqueConstraint,
134 ForeignKeyConstraint,
136}
137
138impl DatabaseErrorKind {
139 pub fn is_transient_locking(self) -> bool {
142 matches!(
143 self,
144 Self::Deadlock | Self::SerializationFailure | Self::LockTimeout
145 )
146 }
147}
148
149pub fn database_error_kind(error: &sea_orm::DbErr) -> Option<DatabaseErrorKind> {
151 use sea_orm::{DbErr, RuntimeErr};
152
153 let sqlx_error = match error {
154 DbErr::Exec(RuntimeErr::SqlxError(error)) | DbErr::Query(RuntimeErr::SqlxError(error)) => {
155 error.as_ref()
156 }
157 _ => return None,
158 };
159 let sea_orm::sqlx::Error::Database(database_error) = sqlx_error else {
160 return None;
161 };
162
163 let mysql_number = database_error
164 .try_downcast_ref::<sea_orm::sqlx::mysql::MySqlDatabaseError>()
165 .map(sea_orm::sqlx::mysql::MySqlDatabaseError::number);
166 let postgres_code = database_error
167 .try_downcast_ref::<sea_orm::sqlx::postgres::PgDatabaseError>()
168 .map(sea_orm::sqlx::postgres::PgDatabaseError::code);
169 let sqlite_code = database_error
170 .try_downcast_ref::<sea_orm::sqlx::sqlite::SqliteError>()
171 .and_then(|error| {
172 use sea_orm::sqlx::error::DatabaseError;
173
174 error.code()
175 })
176 .and_then(|code| code.parse::<i32>().ok());
177 database_error_kind_from_signals(
178 database_error.kind(),
179 mysql_number,
180 postgres_code,
181 sqlite_code,
182 )
183}
184
185fn database_error_kind_from_signals(
186 driver_kind: sea_orm::sqlx::error::ErrorKind,
187 mysql_number: Option<u16>,
188 postgres_code: Option<&str>,
189 sqlite_code: Option<i32>,
190) -> Option<DatabaseErrorKind> {
191 use sea_orm::sqlx::error::ErrorKind;
192
193 match driver_kind {
194 ErrorKind::UniqueViolation => return Some(DatabaseErrorKind::UniqueConstraint),
195 ErrorKind::ForeignKeyViolation => return Some(DatabaseErrorKind::ForeignKeyConstraint),
196 _ => {}
197 }
198 if let Some(number) = mysql_number {
199 match number {
200 1205 => return Some(DatabaseErrorKind::LockTimeout),
201 1213 => return Some(DatabaseErrorKind::Deadlock),
202 _ => {}
203 }
204 }
205 if let Some(code) = postgres_code {
206 match code {
207 "40P01" => return Some(DatabaseErrorKind::Deadlock),
208 "40001" => return Some(DatabaseErrorKind::SerializationFailure),
209 "55P03" => return Some(DatabaseErrorKind::LockTimeout),
210 _ => {}
211 }
212 }
213 match sqlite_code.map(|code| code & 0xFF) {
217 Some(5 | 6) => Some(DatabaseErrorKind::LockTimeout),
218 _ => None,
219 }
220}
221
222#[derive(Debug, thiserror::Error)]
224pub enum DbError {
225 #[error("database connection error: {0}")]
227 DatabaseConnection(String),
228 #[error("database operation error: {0}")]
230 DatabaseOperation(String),
231 #[error("database operation error: {message}")]
233 DatabaseOperationClassified {
234 message: String,
235 kind: DatabaseErrorKind,
236 },
237 #[error("database commit outcome unknown: {message}")]
239 CommitOutcomeUnknown {
240 message: String,
241 kind: Option<DatabaseErrorKind>,
242 },
243 #[error("retry exhausted")]
245 RetryExhausted,
246 #[error("non-retryable error: {0}")]
248 NonRetryable(String),
249}
250
251impl DbError {
252 pub fn database_connection(error: impl std::fmt::Display) -> Self {
254 Self::DatabaseConnection(error.to_string())
255 }
256
257 pub fn database_operation(error: impl std::fmt::Display) -> Self {
259 Self::DatabaseOperation(error.to_string())
260 }
261
262 pub fn database_operation_classified(
264 error: impl std::fmt::Display,
265 kind: DatabaseErrorKind,
266 ) -> Self {
267 Self::DatabaseOperationClassified {
268 message: error.to_string(),
269 kind,
270 }
271 }
272
273 pub fn commit_outcome_unknown(
275 error: impl std::fmt::Display,
276 kind: Option<DatabaseErrorKind>,
277 ) -> Self {
278 Self::CommitOutcomeUnknown {
279 message: error.to_string(),
280 kind,
281 }
282 }
283
284 pub fn database_error_kind(&self) -> Option<DatabaseErrorKind> {
286 match self {
287 Self::DatabaseOperationClassified { kind, .. } => Some(*kind),
288 Self::CommitOutcomeUnknown { kind, .. } => *kind,
289 _ => None,
290 }
291 }
292
293 pub fn commit_outcome_is_unknown(&self) -> bool {
295 matches!(self, Self::CommitOutcomeUnknown { .. })
296 }
297
298 pub fn non_retryable(error: impl std::fmt::Display) -> Self {
300 Self::NonRetryable(error.to_string())
301 }
302
303 pub fn is_retryable(&self) -> bool {
310 match self {
311 Self::DatabaseConnection(_) => true,
312 Self::DatabaseOperationClassified { kind, .. } => kind.is_transient_locking(),
313 _ => false,
314 }
315 }
316}
317
318impl From<sea_orm::DbErr> for DbError {
319 fn from(value: sea_orm::DbErr) -> Self {
320 match database_error_kind(&value) {
321 Some(kind) => Self::database_operation_classified(value, kind),
322 None => Self::database_operation(value),
323 }
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::{DatabaseErrorKind, DbError, database_error_kind_from_signals};
330 use sea_orm::sqlx::error::ErrorKind;
331
332 #[test]
333 fn database_error_kind_covers_common_driver_signals() {
334 assert_eq!(
335 database_error_kind_from_signals(ErrorKind::Other, Some(1213), None, None),
336 Some(DatabaseErrorKind::Deadlock)
337 );
338 assert_eq!(
339 database_error_kind_from_signals(ErrorKind::Other, Some(1205), None, None),
340 Some(DatabaseErrorKind::LockTimeout)
341 );
342 assert_eq!(
343 database_error_kind_from_signals(ErrorKind::Other, None, Some("40P01"), None),
344 Some(DatabaseErrorKind::Deadlock)
345 );
346 assert_eq!(
347 database_error_kind_from_signals(ErrorKind::Other, None, Some("40001"), None),
348 Some(DatabaseErrorKind::SerializationFailure)
349 );
350 assert_eq!(
351 database_error_kind_from_signals(ErrorKind::Other, None, Some("55P03"), None),
352 Some(DatabaseErrorKind::LockTimeout)
353 );
354 }
355
356 #[test]
357 fn database_error_kind_covers_sqlite_busy_and_locked_families() {
358 for code in [5, 6, 261, 517, 262] {
361 assert_eq!(
362 database_error_kind_from_signals(ErrorKind::Other, None, None, Some(code)),
363 Some(DatabaseErrorKind::LockTimeout),
364 "sqlite code {code} should classify as a lock timeout"
365 );
366 }
367 for code in [1, 19, 0] {
369 assert_eq!(
370 database_error_kind_from_signals(ErrorKind::Other, None, None, Some(code)),
371 None,
372 "sqlite code {code} should stay unclassified"
373 );
374 }
375 }
376
377 #[test]
378 fn database_error_kind_prefers_cross_backend_constraint_kind() {
379 assert_eq!(
380 database_error_kind_from_signals(ErrorKind::UniqueViolation, Some(1213), None, None),
381 Some(DatabaseErrorKind::UniqueConstraint)
382 );
383 assert_eq!(
384 database_error_kind_from_signals(ErrorKind::ForeignKeyViolation, None, None, None),
385 Some(DatabaseErrorKind::ForeignKeyConstraint)
386 );
387 assert_eq!(
389 database_error_kind_from_signals(ErrorKind::UniqueViolation, None, None, Some(5)),
390 Some(DatabaseErrorKind::UniqueConstraint)
391 );
392 }
393
394 #[test]
395 fn database_error_kind_ignores_unknown_or_non_driver_signals() {
396 assert_eq!(
397 database_error_kind_from_signals(ErrorKind::Other, Some(9999), Some("99999"), None),
398 None
399 );
400 assert_eq!(
401 super::database_error_kind(&sea_orm::DbErr::Custom("not a driver error".to_string())),
402 None
403 );
404 }
405
406 #[test]
407 fn db_error_constructors_preserve_messages() {
408 assert_eq!(
409 DbError::database_connection("offline").to_string(),
410 "database connection error: offline"
411 );
412 assert_eq!(
413 DbError::database_operation("bad query").to_string(),
414 "database operation error: bad query"
415 );
416 assert_eq!(
417 DbError::non_retryable("invalid config").to_string(),
418 "non-retryable error: invalid config"
419 );
420 assert_eq!(DbError::RetryExhausted.to_string(), "retry exhausted");
421 }
422
423 #[test]
424 fn retryable_classification_matches_error_kind() {
425 assert!(DbError::database_connection("offline").is_retryable());
426 for kind in [
427 DatabaseErrorKind::Deadlock,
428 DatabaseErrorKind::SerializationFailure,
429 DatabaseErrorKind::LockTimeout,
430 ] {
431 assert!(
432 DbError::database_operation_classified("conflict", kind).is_retryable(),
433 "{kind:?} should be retryable"
434 );
435 }
436 assert!(!DbError::database_operation("locked").is_retryable());
438 for kind in [
439 DatabaseErrorKind::UniqueConstraint,
440 DatabaseErrorKind::ForeignKeyConstraint,
441 ] {
442 assert!(
443 !DbError::database_operation_classified("constraint", kind).is_retryable(),
444 "{kind:?} should not be retryable"
445 );
446 }
447 assert!(!DbError::RetryExhausted.is_retryable());
448 assert!(!DbError::non_retryable("invalid config").is_retryable());
449 }
450
451 #[test]
452 fn commit_outcome_unknown_preserves_kind_and_marker() {
453 let error = DbError::commit_outcome_unknown(
454 "connection lost after COMMIT",
455 Some(DatabaseErrorKind::Deadlock),
456 );
457 assert!(error.commit_outcome_is_unknown());
458 assert_eq!(
459 error.database_error_kind(),
460 Some(DatabaseErrorKind::Deadlock)
461 );
462 assert!(!error.is_retryable());
463 }
464
465 #[test]
466 fn sea_orm_errors_are_mapped_to_operation_errors() {
467 let error = DbError::from(sea_orm::DbErr::Custom("custom failure".to_string()));
468
469 assert!(matches!(error, DbError::DatabaseOperation(_)));
470 assert!(error.to_string().contains("custom failure"));
471 }
472}