Summary
The hyperdb-api Error type today is a thin wrapper around hyperdb_api_core::client::Error plus a few high-level variants, with Error::kind() -> Option<ErrorKind> (only the Client variant carries a kind). This shape conflicts with two rules from the Microsoft Pragmatic Rust Guidelines that the project commits to in CONTRIBUTING.md:42. Proposing to flatten the type to the canonical M-ERRORS shape.
Current state
hyperdb-api/src/error.rs:
#[derive(Debug, ThisError)]
#[non_exhaustive]
pub enum Error {
#[error("{0}")]
Client(#[from] hyperdb_api_core::client::Error), // wraps another error enum
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Invalid name: {0}")]
InvalidName(String),
#[error("Invalid table definition: {0}")]
InvalidTableDefinition(String),
#[error("Not found: {0}")]
NotFound(String),
#[error("Already exists: {0}")]
AlreadyExists(String),
#[error("{message}")]
#[non_exhaustive]
Other {
message: String,
#[source]
source: Option<Box<dyn StdError + Send + Sync>>, // dyn escape hatch
},
}
pub fn kind(&self) -> Option<ErrorKind> {
match self {
Error::Client(err) => Some(err.kind()),
_ => None, // most variants → None
}
}
Original gap analysis: §10 of docs/RUST_API_GAP_ANALYSIS.md (predecessor repo) — "ErrorKind is re-exported from hyper-client but its own top-level kind() returns Option<ErrorKind> — makes match-on-error less clean than a single unified ErrorKind."
Microsoft guideline conflicts
CONTRIBUTING.md:42 commits the project to the Microsoft Pragmatic Rust Guidelines. Two rules apply directly:
M-ERRORS-CANONICAL-STRUCTS
Each crate exposes one canonical error type that callers can match on directly. The current shape forces callers to first match on Error::Client(_), then call .kind(), then match on ErrorKind — a two-level match for every database error. Returning Option<ErrorKind> is the API symptom of this two-level nesting.
M-ERRORS-AVOID-WRAPPING-AND-AS-DYN
Other { source: Option<Box<dyn StdError + Send + Sync>> } (error.rs:50) is precisely the Box<dyn StdError> escape hatch the rule discourages. It defeats Send/Sync/'static reasoning at the type level and turns inspection of the underlying cause into a dynamic downcast.
Proposed redesign
Flatten to a single enum whose variants directly express the failure modes — no nested client::Error, no Box<dyn> source.
#[derive(Debug, ThisError)]
#[non_exhaustive]
pub enum Error {
// Connection / transport
#[error("connection error: {0}")]
Connection(String),
#[error("authentication failed: {0}")]
Authentication(String),
#[error("TLS error: {0}")]
Tls(String),
// Protocol / server
#[error("protocol error: {0}")]
Protocol(String),
#[error("server error{}: {message}", sqlstate.as_ref().map(|s| format!(" ({s})")).unwrap_or_default())]
Server { sqlstate: Option<String>, message: String },
// I/O
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
// Catalog / validation (kept from current type)
#[error("invalid name: {0}")]
InvalidName(String),
#[error("invalid table definition: {0}")]
InvalidTableDefinition(String),
#[error("not found: {0}")]
NotFound(String),
#[error("already exists: {0}")]
AlreadyExists(String),
}
impl Error {
pub fn sqlstate(&self) -> Option<&str> { /* from Server variant */ }
// No more `kind()` method — callers match on the enum directly.
}
Concrete changes
Backwards compatibility
Breaking change. Any caller that:
- Pattern-matches on
Error::Client(_), Error::Other { .. },
- Calls
Error::kind(),
- Calls
Error::new / Error::with_cause,
- Imports
hyperdb_api::ErrorKind,
will fail to compile. Migration is mechanical but case-by-case. Released as feat!: per Conventional Commits with a major version bump.
Migration recipe
Before:
match err.kind() {
Some(ErrorKind::Connection) => retry(),
Some(ErrorKind::Authentication) => prompt_creds(),
_ => return Err(err),
}
After:
match err {
Error::Connection(_) => retry(),
Error::Authentication(_) => prompt_creds(),
other => return Err(other),
}
Open questions
- Should
Server carry a structured Position field (line/column from the SQLSTATE diagnostic) instead of just the message? Cleaner, but expands the breaking-change surface.
- Should
Connection / Tls / Authentication carry a typed cause (e.g. #[source] std::io::Error) instead of String? More information, but requires unification with how hyperdb-api-core reports those internally.
Related
Summary
The
hyperdb-apiErrortype today is a thin wrapper aroundhyperdb_api_core::client::Errorplus a few high-level variants, withError::kind() -> Option<ErrorKind>(only theClientvariant carries a kind). This shape conflicts with two rules from the Microsoft Pragmatic Rust Guidelines that the project commits to in CONTRIBUTING.md:42. Proposing to flatten the type to the canonical M-ERRORS shape.Current state
hyperdb-api/src/error.rs:Original gap analysis: §10 of
docs/RUST_API_GAP_ANALYSIS.md(predecessor repo) — "ErrorKindis re-exported fromhyper-clientbut its own top-levelkind()returnsOption<ErrorKind>— makes match-on-error less clean than a single unifiedErrorKind."Microsoft guideline conflicts
CONTRIBUTING.md:42 commits the project to the Microsoft Pragmatic Rust Guidelines. Two rules apply directly:
M-ERRORS-CANONICAL-STRUCTS
Each crate exposes one canonical error type that callers can match on directly. The current shape forces callers to first match on
Error::Client(_), then call.kind(), then match onErrorKind— a two-level match for every database error. ReturningOption<ErrorKind>is the API symptom of this two-level nesting.M-ERRORS-AVOID-WRAPPING-AND-AS-DYN
Other { source: Option<Box<dyn StdError + Send + Sync>> }(error.rs:50) is precisely theBox<dyn StdError>escape hatch the rule discourages. It defeatsSend/Sync/'staticreasoning at the type level and turns inspection of the underlying cause into a dynamic downcast.Proposed redesign
Flatten to a single enum whose variants directly express the failure modes — no nested
client::Error, noBox<dyn>source.Concrete changes
Client(hyperdb_api_core::client::Error)variant. Map eachclient::ErrorKindto a direct top-level variant.Other { message, source: Option<Box<dyn StdError>> }variant. Replace any internal use with a specific variant.Error::kind() -> Option<ErrorKind>. Callers match directly on the flattened enum.Error::new/Error::with_cause(they only exist to populateOther). If a generic-message escape hatch is genuinely needed, add one variant for it (e.g.Internal(String)) rather than reintroducingBox<dyn>.Error::sqlstate()as a convenience accessor on theServervariant.Error::new("…")/Error::with_causecall site in the workspace to use a specific variant.hyperdb_api_core::client::ErrorKindfromhyperdb-api's public API: remove (callers no longer need it).Backwards compatibility
Breaking change. Any caller that:
Error::Client(_),Error::Other { .. },Error::kind(),Error::new/Error::with_cause,hyperdb_api::ErrorKind,will fail to compile. Migration is mechanical but case-by-case. Released as
feat!:per Conventional Commits with a major version bump.Migration recipe
Before:
After:
Open questions
Servercarry a structuredPositionfield (line/column from the SQLSTATE diagnostic) instead of just the message? Cleaner, but expands the breaking-change surface.Connection/Tls/Authenticationcarry a typed cause (e.g.#[source] std::io::Error) instead ofString? More information, but requires unification with howhyperdb-api-corereports those internally.Related
docs/RUST_API_GAP_ANALYSIS.md(predecessor repo).