Skip to main content

chain_builder/
error.rs

1//! Typed errors for fallible compilation ([`QueryBuilder::try_to_sql`]).
2//!
3//! Every fail-loud guard in the compiler maps to one [`BuildError`] variant.
4//! [`QueryBuilder::to_sql`] panics with exactly the [`Display`] message of the
5//! corresponding variant, so the panicking and fallible paths stay in sync.
6//!
7//! [`QueryBuilder::try_to_sql`]: crate::QueryBuilder::try_to_sql
8//! [`QueryBuilder::to_sql`]: crate::QueryBuilder::to_sql
9//! [`Display`]: core::fmt::Display
10
11use core::fmt;
12
13/// Invalid query construction, detected when compiling a
14/// [`QueryBuilder`](crate::QueryBuilder).
15///
16/// Returned by [`try_to_sql`](crate::QueryBuilder::try_to_sql) /
17/// [`try_compile`](crate::try_compile) (and the sqlx handoff twins
18/// `try_to_sqlx_query` / `try_to_sqlx_query_as`). The panicking
19/// [`to_sql`](crate::QueryBuilder::to_sql) path panics with this type's
20/// [`Display`](core::fmt::Display) message.
21///
22/// All variants describe *caller* mistakes (a query that cannot be rendered as
23/// valid SQL), never internal compiler state — a web app can therefore map
24/// errors caused by end-user input (e.g. an operator or pagination parameter
25/// taken from a request) to HTTP 4XX and the rest to 5XX.
26#[derive(Debug, Clone, PartialEq, Eq)]
27#[non_exhaustive]
28pub enum BuildError {
29    /// `for_update()`/`for_share()` on a non-SELECT query.
30    LockRequiresSelect,
31    /// `distinct_on(...)` compiled for a dialect without `DISTINCT ON`.
32    DistinctOnRequiresPostgres,
33    /// `insert()` with no columns.
34    EmptyInsert,
35    /// `update()` with no columns and no `SET` expressions.
36    EmptyUpdate,
37    /// `offset(...)` without `limit(...)`.
38    OffsetWithoutLimit,
39    /// `for_update()`/`for_share()` combined with `UNION`.
40    LockWithUnion,
41    /// `having()` received an operator outside the fixed allowlist. Carries the
42    /// rejected operator as passed by the caller.
43    InvalidHavingOperator(String),
44}
45
46impl fmt::Display for BuildError {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        match self {
49            Self::LockRequiresSelect => {
50                f.write_str("for_update()/for_share() is only valid on SELECT")
51            }
52            Self::DistinctOnRequiresPostgres => f.write_str("DISTINCT ON requires PostgreSQL"),
53            Self::EmptyInsert => f.write_str("insert() requires at least one column"),
54            Self::EmptyUpdate => f.write_str("update() requires at least one column"),
55            Self::OffsetWithoutLimit => f.write_str("offset(...) requires limit(...)"),
56            Self::LockWithUnion => {
57                f.write_str("for_update()/for_share() cannot be combined with UNION")
58            }
59            Self::InvalidHavingOperator(op) => write!(
60                f,
61                "having() operator {op:?} is not an allowed comparison operator \
62                 (use having_raw() for arbitrary aggregate expressions)"
63            ),
64        }
65    }
66}
67
68impl std::error::Error for BuildError {}
69
70/// Unified error for the execution helpers (`fetch_*`, `execute`, `count`):
71/// the query either failed to **build** (invalid construction, see
72/// [`BuildError`]) or failed to **execute** (database/driver error from sqlx).
73///
74/// `From` impls for both sides make `?` work directly in handlers, and
75/// matching on the variant separates caller mistakes (often HTTP 4XX when the
76/// offending input came from a request) from runtime database failures (5XX):
77///
78/// ```ignore
79/// match qb.fetch_all::<Row, _>(&pool).await {
80///     Ok(rows) => ...,
81///     Err(Error::Build(e)) => ...,  // invalid query construction
82///     Err(Error::Sqlx(e)) => ...,   // connection/query/decode failure
83///     Err(e) => ...,                // #[non_exhaustive]: wildcard required downstream
84/// }
85/// ```
86#[derive(Debug)]
87#[non_exhaustive]
88pub enum Error {
89    /// The builder could not be compiled into SQL.
90    Build(BuildError),
91    /// sqlx failed to execute the compiled query.
92    Sqlx(sqlx::Error),
93}
94
95impl fmt::Display for Error {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        match self {
98            Self::Build(e) => e.fmt(f),
99            Self::Sqlx(e) => e.fmt(f),
100        }
101    }
102}
103
104impl std::error::Error for Error {
105    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
106        match self {
107            Self::Build(e) => Some(e),
108            Self::Sqlx(e) => Some(e),
109        }
110    }
111}
112
113impl From<BuildError> for Error {
114    fn from(e: BuildError) -> Self {
115        Self::Build(e)
116    }
117}
118
119impl From<sqlx::Error> for Error {
120    fn from(e: sqlx::Error) -> Self {
121        Self::Sqlx(e)
122    }
123}