Files
Lumotia/crates/storage/src/error.rs
Jake 42f4d07e48
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
agent: lumotia-rebrand — drop MagnotiaError prefix, now lumotia_core::Error
Phase 3 of the rebrand cascade per locked decision D4. MagnotiaError ->
Error in crates/core/src/error.rs (the crate name already qualifies it).
92 usages across 14 .rs files renamed via word-boundary sed.

One collision required disambiguation: lumotia_storage already had its
own local Error type (introduced by the slop-pass Area A residuals work).
crates/storage/src/error.rs aliases the imported core error as CoreError
on import; the From<Error> for CoreError boundary impl and the
CoreError::Storage construction site use the alias.

cargo build --workspace passes. cargo test --workspace: 330 pass, 0 fail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 08:58:05 +01:00

195 lines
6.0 KiB
Rust

//! 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<i64>,
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<Error> 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<T> = std::result::Result<T, Error>;