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}