Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/lance-context-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,7 @@ pub use store::{

// Re-export CompactionMetrics from lance for Python bindings
pub use lance::dataset::optimize::CompactionMetrics;

// Re-export the Lance error type so downstream crates (e.g. the server) can
// match on its typed variants instead of string-matching `Display` output.
pub use lance::Error as LanceError;
74 changes: 64 additions & 10 deletions crates/lance-context-server/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;
use lance_context_api::{ErrorBody, ErrorResponse};
use lance_context_core::LanceError;

#[derive(Debug)]
pub enum AppError {
Expand All @@ -13,16 +14,31 @@ pub enum AppError {
}

impl AppError {
pub fn from_lance(err: impl std::fmt::Display) -> Self {
let msg = err.to_string();
if msg.contains("already in progress") {
AppError::CompactionInProgress
} else if msg.contains("not found") || msg.contains("DatasetNotFound") {
AppError::NotFound(msg)
} else if msg.contains("Invalid") {
AppError::InvalidRequest(msg)
} else {
AppError::Internal(msg)
/// Map a Lance error onto the API's error taxonomy.
///
/// Prefers Lance's **typed** variants over string matching: `DatasetNotFound`
/// / `NotFound` → 404, `DatasetAlreadyExists` → 409, `InvalidInput` /
/// `SchemaMismatch` → 400. The one unavoidable string match is the
/// "compaction already in progress" signal, which the core raises as an
/// `ArrowError::InvalidArgumentError` that Lance folds into its generic
/// `Arrow` variant (no dedicated variant, see `store.rs`); it is checked
/// first so it still maps to 409 `COMPACTION_IN_PROGRESS`.
pub fn from_lance(err: LanceError) -> Self {
// Checked before the typed match: the compaction-in-progress signal is an
// Arrow-wrapped error with no dedicated variant, so only its text
// distinguishes it.
if err.to_string().contains("already in progress") {
return AppError::CompactionInProgress;
}
match err {
LanceError::DatasetNotFound { .. } | LanceError::NotFound { .. } => {
AppError::NotFound(err.to_string())
}
LanceError::DatasetAlreadyExists { .. } => AppError::AlreadyExists(err.to_string()),
LanceError::InvalidInput { .. } | LanceError::SchemaMismatch { .. } => {
AppError::InvalidRequest(err.to_string())
}
other => AppError::Internal(other.to_string()),
}
}
}
Expand Down Expand Up @@ -51,3 +67,41 @@ impl IntoResponse for AppError {
(status, Json(body)).into_response()
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn maps_typed_variants_to_taxonomy() {
assert!(matches!(
AppError::from_lance(LanceError::dataset_not_found("db/x", "missing".into())),
AppError::NotFound(_)
));
assert!(matches!(
AppError::from_lance(LanceError::dataset_already_exists("db/x")),
AppError::AlreadyExists(_)
));
assert!(matches!(
AppError::from_lance(LanceError::invalid_input("bad field")),
AppError::InvalidRequest(_)
));
assert!(matches!(
AppError::from_lance(LanceError::io("disk gone".to_string())),
AppError::Internal(_)
));
}

#[test]
fn compaction_in_progress_is_detected_from_arrow_variant() {
// Reproduce how the core raises it: an ArrowError folded into Lance's
// generic `Arrow` variant (NOT `InvalidInput`), so only the text
// identifies it. `LanceError::arrow` is exactly what `From<ArrowError>`
// produces for a non-external ArrowError.
let err = LanceError::arrow("Compaction already in progress");
assert!(matches!(
AppError::from_lance(err),
AppError::CompactionInProgress
));
}
}
Loading