// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0
///|
/// A coarse failure class, so a caller can branch on *what kind* of failure
/// happened without parsing a message or knowing which backend produced it. The
/// classes follow Python DB-API 2.0's exception tree (PEP 249), collapsed to the
/// ones a driver can derive from a SQLSTATE or a vendor error code.
///
/// * `Interface` — the client API was used wrongly: a closed handle, a row
/// index out of range, a value read as the wrong type.
/// * `Database` — a generic server-side failure the finer classes do not cover.
/// * `Data` — a value is out of range, badly formed, or cannot be cast.
/// * `Operational` — the connection, a lock, a transaction's state, or a resource
/// limit: the class a retry or a reconnect may clear.
/// * `Integrity` — a constraint (unique, foreign key, not-null, check) was violated.
/// * `Programming` — the SQL or its references are wrong: syntax, unknown table or
/// column, insufficient privilege.
/// * `Internal` — the backend reports its own internal inconsistency.
/// * `NotSupported` — the backend or driver does not implement the feature.
#derive(Eq, Debug)
pub(all) enum ErrorKind {
Interface
Database
Data
Operational
Integrity
Programming
Internal
NotSupported
}
///|
pub extend ErrorKind with Eq::{not_equal, equal}
///|
pub extend ErrorKind with Debug::{to_repr}
///|
/// The lowercase name of the class, for logs and test output.
pub fn ErrorKind::to_string(self : ErrorKind) -> String {
match self {
Interface => "interface"
Database => "database"
Data => "data"
Operational => "operational"
Integrity => "integrity"
Programming => "programming"
Internal => "internal"
NotSupported => "not-supported"
}
}
///|
/// What a server-reported failure carries beyond its message: the five-character
/// SQLSTATE, the vendor error number, and the server's severity, detail and hint.
/// Drivers fill what their protocol supplies and leave the rest `None`. The coarse
/// [`ErrorKind`] is derived from the SQLSTATE (and, for MySQL's generic `HY000`,
/// from the errno).
pub(all) struct ServerDetail {
kind : ErrorKind
sqlstate : String?
code : Int32?
message : String
detail : String?
hint : String?
severity : String?
}
///|
/// A database-layer failure. Every fallible operation on the interface raises this
/// one error type, so a query layer catches a single kind. The cases mirror the
/// coarse failure classes both Go's `database/sql` and Python's DB-API 2.0 draw a
/// line around, kept deliberately few so drivers can classify without a taxonomy:
///
/// * `ConnectError` — opening, attaching, or handshaking a connection failed, or
/// the connection dropped mid-flight.
/// * `QueryError` — the backend rejected a statement, or the driver hit a
/// protocol error, without a structured server response to report.
/// * `TypeError` — a [`Row`] typed accessor was asked to read a column as a type
/// it does not hold (e.g. `int` on a `Text`), or a column index/name that does
/// not exist in the row.
/// * `Closed` — the connection or statement was used after `close`.
/// * `Server` — the server rejected the statement and reported a SQLSTATE
/// and/or a numeric error code; see [`ServerDetail`] and [`DbError::kind`].
///
/// Declared `pub(all)` so out-of-tree drivers (moonpostgres, moonmysql, …) that
/// live in their own packages can *construct and raise* these cases — a plain `pub`
/// suberror would let them catch a `DbError` but not build one.
pub(all) suberror DbError {
ConnectError(String)
QueryError(String)
TypeError(String)
Closed
Server(ServerDetail)
}
///|
/// A one-line rendering of the error, e.g. `QueryError: no such table: hero` or
/// `ServerError: relation "hero" does not exist [42P01]`.
pub fn DbError::to_string(self : DbError) -> String {
match self {
ConnectError(m) => "ConnectError: " + m
QueryError(m) => "QueryError: " + m
TypeError(m) => "TypeError: " + m
Closed => "Closed: operation on a closed connection"
Server(d) => {
let mut s = "ServerError: " + d.message
match d.sqlstate {
Some(st) => s = s + " [" + st + "]"
None => ()
}
match d.code {
Some(code) => s = s + " (code " + code.to_string() + ")"
None => ()
}
s
}
}
}
///|
/// The failure class of any `DbError`, including the ones that carry no server
/// response. `ConnectError` is operational (a retry may succeed); a plain
/// `QueryError` is a generic database failure; a `TypeError` or `Closed` is
/// interface misuse; a structured `Server` reports the class its SQLSTATE implies.
pub fn DbError::kind(self : DbError) -> ErrorKind {
match self {
ConnectError(_) => Operational
QueryError(_) => Database
TypeError(_) => Interface
Closed => Interface
Server(d) => d.kind
}
}
///|
/// The five-character SQLSTATE the server reported, or `None` when the failure
/// did not come from a structured server response.
pub fn DbError::sqlstate(self : DbError) -> String? {
match self {
Server(d) => d.sqlstate
_ => None
}
}
///|
/// The vendor error number the server reported (MySQL's errno), or `None`.
pub fn DbError::code(self : DbError) -> Int32? {
match self {
Server(d) => d.code
_ => None
}
}
///|
/// Classify a five-character SQLSTATE into an [`ErrorKind`] using the class (its
/// first two characters), per the SQL:2016 / PostgreSQL table of class codes.
/// Unknown, empty, or short states fall back to `Database`.
pub fn classify_sqlstate(state : String) -> ErrorKind {
if state.length() < 2 {
return Database
}
let class = state[0:2].to_owned()
if class == "08" {
Operational // connection exception
} else if class == "0A" {
NotSupported // feature not supported
} else if class == "0B" {
Operational // invalid transaction initiation
} else if class == "21" || class == "22" {
Data // cardinality / data exception
} else if class == "23" {
Integrity // integrity constraint violation
} else if class == "25" {
Operational // invalid transaction state
} else if class == "26" || class == "27" || class == "2B" || class == "2D" {
Programming // invalid statement / triggered data change / dependent privilege
} else if class == "2F" {
Internal // SQL routine exception
} else if class == "28" {
Operational // invalid authorization specification
} else if class == "34" || class == "3B" || class == "3D" || class == "3F" {
Programming // invalid cursor / savepoint / catalog / schema
} else if class == "38" || class == "39" {
Internal // external routine exception / external routine invocation exception
} else if class == "40" {
Operational // transaction rollback (deadlock, serialization failure)
} else if class == "42" || class == "44" {
Programming // syntax or access rule violation / WITH CHECK OPTION
} else if class == "53" || class == "54" || class == "55" || class == "57" {
Operational // insufficient resources / program limit / object state / intervention
} else if class == "58" || class == "F0" || class == "XX" {
Internal // system error / configuration / internal error
} else {
Database
}
}
///|
/// Prints the failure with the detail the driver gave, so a log line says which
/// statement failed rather than only that one did.
pub impl Show for DbError with fn output(self : DbError, logger : &Logger) -> Unit {
logger.write_string(self.to_string())
}
///|
pub extend DbError with Show::{output}