tidaldb/tidal/src/query/retrieve/errors.rs
jordan 192c473f55 feat: complete Milestone 5 — full-text search, RRF fusion, and creator search
- M5p1: BM25 text indexing via Tantivy with background syncer (0.26ms @ 10K docs)
- M5p2: RRF fusion layer combining BM25 + ANN scores (46µs @ 1K candidates)
- M5p3: unified Search query API (8-stage pipeline, BM25 + vector + ranking)
- M5p4: creator text + vector indexing and creator search executor (< 20ms @ 200 creators)
- Refactor db/mod.rs into focused sub-modules (creators, items, sessions, signals, etc.)
- Decompose monolithic files into directory modules (query/executor, ranking/diversity, etc.)
- Split brute.rs → brute/mod.rs + brute/tests.rs; extract search executor helpers
- Add benches: fusion, search, session, text_index
- Add M5 UAT test suites (m5_uat, m5_search, m5p4_creator_search, text_index)
- Update blog posts, roadmap, content strategy, and M5 planning docs
- Add tmp/ and .claude/worktrees/ to .gitignore

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-21 23:53:16 -07:00

103 lines
3.6 KiB
Rust

//! Error types for RETRIEVE query construction, validation, and execution.
/// Errors arising from query construction, validation, or execution.
///
/// Replaces the Milestone 1 stub `QueryError { message }`. Each variant
/// carries structured context so callers can programmatically handle
/// specific failure modes.
#[derive(Debug, thiserror::Error)]
pub enum QueryError {
/// The named ranking profile does not exist in the registry.
#[error("profile '{0}' not found")]
ProfileNotFound(String),
/// A filter references an invalid field or has an unsupported value.
#[error("invalid filter on '{field}': {reason}")]
InvalidFilter { field: String, reason: String },
/// The requested limit is outside the allowed range.
#[error("limit {requested} out of range [{min}, {max}]")]
InvalidLimit {
requested: usize,
min: usize,
max: usize,
},
/// A required index (vector, text, bitmap) is not available.
#[error("index '{0}' not available")]
IndexNotAvailable(String),
/// The underlying storage engine returned an error.
#[error("storage error: {0}")]
StorageError(String),
/// A pagination cursor could not be decoded.
#[error("invalid cursor: {0}")]
InvalidCursor(String),
/// The query requires a candidate strategy not yet implemented.
#[error("unsupported strategy: {0}")]
UnsupportedStrategy(String),
/// The FOR SESSION clause references a session that does not exist.
#[error("session not found: {0}")]
SessionNotFound(String),
}
// Manual `From` because this is a lossy conversion: `StorageError` is
// flattened to its `Display` string. The variant holds `String`, not
// `StorageError`, so `#[from]` cannot be used.
impl From<crate::storage::StorageError> for QueryError {
fn from(e: crate::storage::StorageError) -> Self {
Self::StorageError(e.to_string())
}
}
// ── Tests ───────────────────────────────────────────────────────────────────
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::storage::StorageError;
#[test]
fn query_error_display_profile_not_found() {
let e = QueryError::ProfileNotFound("trending".into());
assert!(e.to_string().contains("trending"));
assert!(e.to_string().contains("not found"));
}
#[test]
fn query_error_display_invalid_filter() {
let e = QueryError::InvalidFilter {
field: "category".into(),
reason: "unknown value".into(),
};
assert!(e.to_string().contains("category"));
}
#[test]
fn query_error_display_invalid_limit() {
let e = QueryError::InvalidLimit {
requested: 0,
min: 1,
max: 500,
};
assert!(e.to_string().contains('0'));
}
#[test]
fn query_error_display_invalid_cursor() {
let e = QueryError::InvalidCursor("bad data".into());
assert!(e.to_string().contains("bad data"));
}
#[test]
fn query_error_display_unsupported_strategy() {
let e = QueryError::UnsupportedStrategy("Hybrid requires M3".into());
assert!(e.to_string().contains("Hybrid"));
}
#[test]
fn query_error_from_storage_error() {
let storage_err = StorageError::Closed;
let query_err: QueryError = storage_err.into();
assert!(matches!(query_err, QueryError::StorageError(_)));
assert!(query_err.to_string().contains("storage"));
}
}