//! Storage-local typed error. //! //! Backend code that wants to branch on storage failure modes pattern-matches //! on [`Error`] directly. The crate boundary into [`lumotia_core::error::Error`] //! flattens this into a [`Error::Storage`] variant carrying a serde-friendly //! `kind`, an `operation` label, and the Display output as `detail` — so the //! frontend (which still receives stringified errors from Tauri commands today) //! keeps working, and Area E can later expose the typed `kind` to the FE without //! touching call sites. //! //! The `sqlx::Error` source is deliberately not serialized. Sources stay sources; //! only the boundary shape is wire-friendly. use std::borrow::Cow; use std::path::PathBuf; use lumotia_core::error::{Error as CoreError, StorageKind}; /// Kinds of database-open operation that can fail before the pool is ready. #[derive(Debug, Clone, Copy)] pub enum OpenOp { Connect, ReadOnlyConnect, ForeignKeysPragma, } impl OpenOp { fn label(self) -> &'static str { match self { OpenOp::Connect => "connect", OpenOp::ReadOnlyConnect => "read_only_connect", OpenOp::ForeignKeysPragma => "foreign_keys_pragma", } } } impl std::fmt::Display for OpenOp { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(self.label()) } } /// Per-step phase inside the migration framework. #[derive(Debug, Clone, Copy)] pub enum MigrationStep { SchemaVersionTableCreate, SchemaVersionQuery, TxBegin, Apply, RecordVersion, Commit, } impl MigrationStep { fn label(self) -> &'static str { match self { MigrationStep::SchemaVersionTableCreate => "schema_version_table_create", MigrationStep::SchemaVersionQuery => "schema_version_query", MigrationStep::TxBegin => "tx_begin", MigrationStep::Apply => "apply", MigrationStep::RecordVersion => "record_version", MigrationStep::Commit => "commit", } } } impl std::fmt::Display for MigrationStep { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(self.label()) } } /// Entities the storage layer can fail to find or validate. /// /// Seeded with only the three real entities (Transcript, Task, Profile) — add /// more here only when a typed `NotFound` / `InvalidReference` case actually /// requires them. #[derive(Debug, Clone, Copy)] pub enum Entity { Transcript, Task, Profile, ImplementationRule, Feedback, } impl Entity { fn label(self) -> &'static str { match self { Entity::Transcript => "transcript", Entity::Task => "task", Entity::Profile => "profile", Entity::ImplementationRule => "implementation_rule", Entity::Feedback => "feedback", } } } impl std::fmt::Display for Entity { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(self.label()) } } /// Storage-crate-local typed error. #[derive(Debug, thiserror::Error)] pub enum Error { #[error("database open failed during {operation}: {source}")] DatabaseOpen { operation: OpenOp, #[source] source: sqlx::Error, }, #[error("migration step {step} (version {version:?}) failed: {source}")] Migration { version: Option, step: MigrationStep, #[source] source: sqlx::Error, }, #[error("query failed during {operation}: {source}")] Query { operation: Cow<'static, str>, #[source] source: sqlx::Error, }, #[error("{entity} not found: '{key}'")] NotFound { entity: Entity, key: String }, #[error("invalid {entity} reference: {reason}")] InvalidReference { entity: Entity, reason: Cow<'static, str>, }, #[error("filesystem operation failed at '{}': {source}", path.display())] Filesystem { path: PathBuf, #[source] source: std::io::Error, }, } impl Error { /// Discriminator for the boundary conversion into [`Error`]. pub fn kind(&self) -> StorageKind { match self { Error::DatabaseOpen { .. } => StorageKind::DatabaseOpen, Error::Migration { .. } => StorageKind::Migration, Error::Query { .. } => StorageKind::Query, Error::NotFound { .. } => StorageKind::NotFound, Error::InvalidReference { .. } => StorageKind::InvalidReference, Error::Filesystem { .. } => StorageKind::Filesystem, } } /// Operation label that flows into the [`Error::Storage`] boundary /// shape. For per-query failures this is the caller-provided label; /// for the other variants it's the variant's intrinsic label. pub fn operation_label(&self) -> Cow<'static, str> { match self { Error::DatabaseOpen { operation, .. } => Cow::Borrowed(operation.label()), Error::Migration { step, .. } => Cow::Borrowed(step.label()), Error::Query { operation, .. } => operation.clone(), Error::NotFound { entity, .. } => Cow::Owned(format!("not_found:{}", entity.label())), Error::InvalidReference { entity, .. } => { Cow::Owned(format!("invalid_reference:{}", entity.label())) } Error::Filesystem { .. } => Cow::Borrowed("filesystem"), } } } /// Boundary conversion — flattens the typed error into the wire-friendly /// [`CoreError::Storage`] variant. Lives in the storage crate (not in core) /// to avoid a `core -> storage` dependency cycle. impl From for CoreError { fn from(error: Error) -> Self { let kind = error.kind(); let operation = error.operation_label().into_owned(); let detail = error.to_string(); CoreError::Storage { kind, operation, detail, } } } /// `Result` alias for storage-crate functions. pub type Result = std::result::Result;