Skip to main content

qbrs_sqlx/
lib.rs

1//! Execution integration between qbrs's query builder and a real Postgres
2//! via `sqlx`. This crate owns only the value-binding and row-decoding glue;
3//! query building, SQL rendering, and every compile-time guarantee live in
4//! `qbrs-core`, which stays independent of any async runtime or driver.
5
6use qbrs_core::delete::Delete;
7use qbrs_core::dialect::Postgres;
8use qbrs_core::expr::Value;
9use qbrs_core::insert::Insert;
10use qbrs_core::row::{Row, RowCons, RowNil};
11use qbrs_core::select::{DynSelect, Prepared, PreparedParams, Select, Selection, SetOp, Total};
12use qbrs_core::statement::{Returning, Statement, WrittenTable};
13use qbrs_core::update::Update;
14use sqlx::Row as _;
15use sqlx::postgres::PgRow;
16
17/// Errors from executing a qbrs query against Postgres via `sqlx`. An enum
18/// rather than a bare `sqlx::Error` so a qbrs-level misuse is distinguishable
19/// from a driver/database error without string-matching a message.
20#[derive(Debug, thiserror::Error)]
21pub enum Error {
22    /// A real error from Postgres or the `sqlx` driver: a failed
23    /// connection, constraint violation, decode failure, etc.
24    #[error(transparent)]
25    Sqlx(#[from] sqlx::Error),
26
27    /// A `prepare!{}` placeholder reached execution unresolved: either
28    /// `Prepared::resolve()` found no matching field in `Params`, or the
29    /// query was executed directly instead of through `.prepare()`.
30    #[error(transparent)]
31    UnresolvedPlaceholder(#[from] qbrs_core::select::UnresolvedPlaceholder),
32
33    /// An `*Update` describing no assignment, or an insert of no rows —
34    /// caught where the request-shaped data is read (`Assignments::from_row`,
35    /// `.values_all`), never at a statement. Here so a handler returning
36    /// this crate's `Result` can `?` on that as readily as on a query.
37    #[error(transparent)]
38    NothingToSet(#[from] qbrs_core::update::NothingToSet),
39
40    #[error(transparent)]
41    NothingToInsert(#[from] qbrs_core::insert::NothingToInsert),
42
43    /// A column type is enabled on `qbrs` but not on `qbrs-sqlx`, so the
44    /// value renders and has nothing to bind it. The two crates carry the
45    /// same feature names for exactly this reason — turn it on in both.
46    #[error("`{0}` values need the matching feature on `qbrs-sqlx` too")]
47    FeatureNotEnabled(&'static str),
48}
49
50/// This crate's `Result`: the same shape as `sqlx::Result`, with
51/// `qbrs_sqlx::Error` as the fixed error type.
52pub type Result<T> = std::result::Result<T, Error>;
53
54/// Every extension trait that puts a terminal method on a builder, plus the
55/// error type a caller's own signatures have to name and the `DecodeRow`
56/// bound a generic helper over `RowQuery` has to spell. `Result` is
57/// deliberately absent: a glob-imported alias of that name shadows
58/// `std::result::Result` in every module that follows, and a service layer
59/// has its own error type in most of them — write `qbrs_sqlx::Result<T>`
60/// where the alias is wanted. Which trait applies
61/// depends on the builder, so importing them one at a time is bookkeeping
62/// with no decision in it — and `count` in particular resolves against
63/// `Iterator::count` with a confusing message until `CountExt` is in scope.
64pub mod prelude {
65    pub use crate::Error;
66    pub use crate::{
67        CountExt, CountQuery, DecodeRow, ExecuteExt, LoadExt, PreparedCountExt, PreparedExt,
68        PreparedQuery, PreparedTotal, RowQuery, WriteStatement,
69    };
70}
71
72/// Binds a `Value` to a Postgres query parameter. `Value`'s typed `NullX`
73/// variants carry the parameter type a NULL bind still has to declare.
74///
75/// Fallible only for `Value::Placeholder`: an unresolved named placeholder
76/// is a misuse no compile-time check here can catch, so it surfaces as
77/// `Error::UnresolvedPlaceholder` rather than a wrong bind or a panic.
78fn bind_value<'q>(
79    query: sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
80    v: Value,
81) -> Result<sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>> {
82    Ok(match v {
83        Value::I32(x) => query.bind(x),
84        Value::I64(x) => query.bind(x),
85        Value::F64(x) => query.bind(x),
86        Value::Text(x) => query.bind(x),
87        Value::Bool(x) => query.bind(x),
88        Value::Bytes(x) => query.bind(x),
89        Value::NullI32 => query.bind(None::<i32>),
90        Value::NullI64 => query.bind(None::<i64>),
91        Value::NullF64 => query.bind(None::<f64>),
92        Value::NullText => query.bind(None::<String>),
93        Value::NullBool => query.bind(None::<bool>),
94        Value::NullBytes => query.bind(None::<Vec<u8>>),
95        #[cfg(feature = "chrono")]
96        Value::Timestamptz(x) => query.bind(x),
97        #[cfg(feature = "chrono")]
98        Value::NullTimestamptz => query.bind(None::<chrono::DateTime<chrono::Utc>>),
99        #[cfg(feature = "chrono")]
100        Value::Date(x) => query.bind(x),
101        #[cfg(feature = "chrono")]
102        Value::NullDate => query.bind(None::<chrono::NaiveDate>),
103        #[cfg(feature = "uuid")]
104        Value::Uuid(x) => query.bind(x),
105        #[cfg(feature = "uuid")]
106        Value::NullUuid => query.bind(None::<uuid::Uuid>),
107        #[cfg(feature = "decimal")]
108        Value::Numeric(x) => query.bind(x),
109        #[cfg(feature = "decimal")]
110        Value::NullNumeric => query.bind(None::<rust_decimal::Decimal>),
111        Value::Placeholder(name) => {
112            return Err(qbrs_core::select::UnresolvedPlaceholder(name).into());
113        }
114        // Reachable only when a column type is on in `qbrs-core` and off
115        // here: the variant exists, the arm that binds it doesn't.
116        #[allow(unreachable_patterns)]
117        other => return Err(Error::FeatureNotEnabled(other.type_name())),
118    })
119}
120
121fn bind_all<'q>(
122    mut query: sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>,
123    params: Vec<Value>,
124) -> Result<sqlx::query::Query<'q, sqlx::Postgres, sqlx::postgres::PgArguments>> {
125    for p in params {
126        query = bind_value(query, p)?;
127    }
128    Ok(query)
129}
130
131/// Generic over `E: sqlx::PgExecutor` so every `.load()`/`.execute()` works
132/// against a `&PgPool` or a transaction alike. sqlx implements `Executor` for
133/// `&mut PgConnection`, not `Transaction`, so callers pass `&mut *tx`.
134async fn fetch_all<'e, T: DecodeRow, E: sqlx::PgExecutor<'e>>(
135    executor: E,
136    sql: &str,
137    params: Vec<Value>,
138) -> Result<Vec<T>> {
139    let rows = bind_all(sqlx::query(sqlx::AssertSqlSafe(sql)), params)?
140        .fetch_all(executor)
141        .await?;
142    rows.iter()
143        .map(|row| T::decode_at(row, &mut 0).map_err(Error::from))
144        .collect()
145}
146
147async fn fetch_optional<'e, T: DecodeRow, E: sqlx::PgExecutor<'e>>(
148    executor: E,
149    sql: &str,
150    params: Vec<Value>,
151) -> Result<Option<T>> {
152    let row = bind_all(sqlx::query(sqlx::AssertSqlSafe(sql)), params)?
153        .fetch_optional(executor)
154        .await?;
155    row.as_ref()
156        .map(|r| T::decode_at(r, &mut 0).map_err(Error::from))
157        .transpose()
158}
159
160async fn execute_only<'e, E: sqlx::PgExecutor<'e>>(
161    executor: E,
162    sql: &str,
163    params: Vec<Value>,
164) -> Result<u64> {
165    let result = bind_all(sqlx::query(sqlx::AssertSqlSafe(sql)), params)?
166        .execute(executor)
167        .await?;
168    Ok(result.rows_affected())
169}
170
171/// What a row-producing query renders to, and what its rows decode to: a
172/// `SELECT`, a `RETURNING` clause, an erased `DynSelect`, a `UNION` chain.
173/// `LoadExt` is the pair of methods over it, and the split is load-bearing
174/// — with the validity bound on the impl instead, an invalid selection
175/// makes `.load(..)` not exist, and the scope error the builder wanted to
176/// report is replaced by a method-resolution failure that never mentions
177/// the table.
178///
179/// `Idx` is threaded through the trait's parameter list for the reason
180/// `scope::Superset` explains. Callers never see it; it's inferred.
181#[diagnostic::on_unimplemented(
182    message = "`{Self}` isn't a query this crate can run",
183    label = "a `Select`, a `RETURNING`, a `DynSelect` or a set operation, in the `Postgres` dialect, whose values are all types `DecodeRow` covers"
184)]
185pub trait RowQuery<Idx> {
186    type Output: DecodeRow;
187
188    #[doc(hidden)]
189    fn rendered(&self) -> (String, Vec<Value>);
190}
191
192/// `load` for the rows, `load_one` for the first of them, and
193/// `ExecuteExt::execute` where there are none to decode. One trait for
194/// every row-producing builder keeps the terminal vocabulary tied to what a
195/// statement yields rather than to which builder happens to be in hand.
196///
197/// Implemented for every builder, satisfiable by the ones that produce
198/// rows: what a builder can't do is then reported by `RowQuery`, which says
199/// so, rather than by the method not existing — which rustc answers with a
200/// list of unsatisfied bounds or, worse, by suggesting `Iterator`. Not a
201/// blanket impl, since `load`/`count`/`execute` are names other traits in a
202/// caller's scope have too. A builder added here needs its three empty
203/// impls, or its terminal goes back to reporting nothing.
204pub trait LoadExt {
205    fn load<'e, Idx, E: sqlx::PgExecutor<'e>>(
206        &self,
207        executor: E,
208    ) -> impl std::future::Future<Output = Result<Vec<<Self as RowQuery<Idx>>::Output>>>
209    where
210        Self: RowQuery<Idx>,
211    {
212        let (sql, params) = self.rendered();
213        async move { fetch_all::<<Self as RowQuery<Idx>>::Output, E>(executor, &sql, params).await }
214    }
215
216    fn load_one<'e, Idx, E: sqlx::PgExecutor<'e>>(
217        &self,
218        executor: E,
219    ) -> impl std::future::Future<Output = Result<Option<<Self as RowQuery<Idx>>::Output>>>
220    where
221        Self: RowQuery<Idx>,
222    {
223        let (sql, params) = self.rendered();
224        async move { fetch_optional::<<Self as RowQuery<Idx>>::Output, E>(executor, &sql, params).await }
225    }
226}
227
228impl<D, Scope, Sel, Outer> LoadExt for Select<D, Scope, Sel, Outer> {}
229impl<S, Sel> LoadExt for Returning<S, Sel> {}
230impl<D, Output> LoadExt for DynSelect<D, Output> {}
231impl<D, Output> LoadExt for SetOp<D, Output> {}
232impl<D, R: qbrs_core::insert::InsertRow> LoadExt for Insert<D, R> {}
233impl<D, T: qbrs_core::scope::Table> LoadExt for Update<D, T> {}
234impl<D, T: qbrs_core::scope::Table> LoadExt for Delete<D, T> {}
235
236impl<Scope, Sel, Idx> RowQuery<Idx> for Select<Postgres, Scope, Sel>
237where
238    Sel: Selection<Scope, Idx>,
239    Sel::Output: DecodeRow,
240{
241    type Output = Sel::Output;
242
243    fn rendered(&self) -> (String, Vec<Value>) {
244        self.to_sql::<Idx>(Postgres)
245    }
246}
247
248/// `SELECT count(*)` over a query's `FROM`/`JOIN`/`WHERE`/`GROUP BY`, with
249/// its `ORDER BY`/`LIMIT`/`OFFSET` dropped — a total counts the rows that
250/// match, not the page being shown. Returns a number rather than an
251/// `Option`, since a count query always produces exactly one row.
252#[diagnostic::on_unimplemented(
253    message = "`{Self}` isn't a query this crate can count",
254    label = "a `Select`, a `DynSelect` or a set operation in the `Postgres` dialect is; a writing statement reports rows affected through `.execute(..)` instead"
255)]
256pub trait CountQuery<Idx> {
257    #[doc(hidden)]
258    fn count_rendered(&self) -> (String, Vec<Value>);
259}
260
261/// The bound is on the method, and the impls are per-builder, for the two
262/// reasons `LoadExt` explains.
263pub trait CountExt {
264    fn count<'e, Idx, E: sqlx::PgExecutor<'e>>(
265        &self,
266        executor: E,
267    ) -> impl std::future::Future<Output = Result<i64>>
268    where
269        Self: CountQuery<Idx>,
270    {
271        count_rows(executor, self.count_rendered())
272    }
273}
274
275impl<D, Scope, Sel, Outer> CountExt for Select<D, Scope, Sel, Outer> {}
276impl<S, Sel> CountExt for Returning<S, Sel> {}
277impl<D, Output> CountExt for DynSelect<D, Output> {}
278impl<D, Output> CountExt for SetOp<D, Output> {}
279impl<D, R: qbrs_core::insert::InsertRow> CountExt for Insert<D, R> {}
280impl<D, T: qbrs_core::scope::Table> CountExt for Update<D, T> {}
281impl<D, T: qbrs_core::scope::Table> CountExt for Delete<D, T> {}
282
283impl<Scope, Sel: Selection<Scope, Idx>, Idx> CountQuery<Idx> for Select<Postgres, Scope, Sel> {
284    fn count_rendered(&self) -> (String, Vec<Value>) {
285        self.count_sql::<Idx>(Postgres)
286    }
287}
288
289/// Erasure is for a query whose joins depend on a condition, and such a
290/// query is paged like any other, so it counts like any other. The same
291/// goes for a set-operation chain.
292impl<Output> CountQuery<()> for DynSelect<Postgres, Output> {
293    fn count_rendered(&self) -> (String, Vec<Value>) {
294        self.count_sql(Postgres)
295    }
296}
297
298impl<Output> CountQuery<()> for SetOp<Postgres, Output> {
299    fn count_rendered(&self) -> (String, Vec<Value>) {
300        self.count_sql(Postgres)
301    }
302}
303
304async fn count_rows<'e, E: sqlx::PgExecutor<'e>>(
305    executor: E,
306    (sql, params): (String, Vec<Value>),
307) -> Result<i64> {
308    let row = bind_all(sqlx::query(sqlx::AssertSqlSafe(sql.as_str())), params)?
309        .fetch_one(executor)
310        .await?;
311    Ok(row.try_get::<i64, _>(0)?)
312}
313
314/// Every writing statement, rendered: what `execute` returns is rows
315/// affected, whichever of the three it was.
316#[diagnostic::on_unimplemented(
317    message = "`{Self}` isn't a statement this crate can execute",
318    label = "an `INSERT`, `UPDATE` or `DELETE` in the `Postgres` dialect is; a `SELECT` or a `RETURNING` yields rows, so it goes through `.load(..)` — and a prepared query through `.load(.., params)`"
319)]
320pub trait WriteStatement {
321    #[doc(hidden)]
322    fn write_rendered(&self) -> (String, Vec<Value>);
323}
324
325#[diagnostic::do_not_recommend]
326impl<S: Statement<Dialect = Postgres>> WriteStatement for S {
327    fn write_rendered(&self) -> (String, Vec<Value>) {
328        self.to_sql(Postgres)
329    }
330}
331
332/// The bound is on the method, and the impls are per-builder, for the two
333/// reasons `LoadExt` explains.
334pub trait ExecuteExt {
335    fn execute<'e, E: sqlx::PgExecutor<'e>>(
336        &self,
337        executor: E,
338    ) -> impl std::future::Future<Output = Result<u64>>
339    where
340        Self: WriteStatement,
341    {
342        let (sql, params) = self.write_rendered();
343        async move { execute_only(executor, &sql, params).await }
344    }
345}
346
347impl<D, Scope, Sel, Outer> ExecuteExt for Select<D, Scope, Sel, Outer> {}
348impl<S, Sel> ExecuteExt for Returning<S, Sel> {}
349impl<D, Output> ExecuteExt for DynSelect<D, Output> {}
350impl<D, Output> ExecuteExt for SetOp<D, Output> {}
351impl<D, R: qbrs_core::insert::InsertRow> ExecuteExt for Insert<D, R> {}
352impl<D, T: qbrs_core::scope::Table> ExecuteExt for Update<D, T> {}
353impl<D, T: qbrs_core::scope::Table> ExecuteExt for Delete<D, T> {}
354
355/// One impl for every `RETURNING`: what a statement returns is decided by
356/// its selection, not by which statement it was.
357impl<S: Statement<Dialect = Postgres>, Sel, Idx> RowQuery<Idx> for Returning<S, Sel>
358where
359    Sel: Selection<WrittenTable<S::Table>, Idx>,
360    Sel::Output: DecodeRow,
361{
362    type Output = Sel::Output;
363    fn rendered(&self) -> (String, Vec<Value>) {
364        self.to_sql(Postgres)
365    }
366}
367
368/// Decodes a query's `Output` positionally out of a `PgRow`. Keyed on the
369/// plain-Rust type a selection produces rather than on the selection
370/// itself: erasure leaves only `Output`, with no `Selection` impl left to
371/// hang decoding off, so this is implemented directly against the closed set
372/// of native types.
373#[diagnostic::on_unimplemented(
374    message = "`{Self}` isn't a value this crate can decode",
375    label = "every selected column has to decode to one of the six built-in natives, or to a type whose feature is on here as well as on `qbrs`",
376    note = "`chrono`/`uuid`/`decimal` have to be enabled on `qbrs-sqlx` too — they are separate `cfg`s over one `Value`"
377)]
378pub trait DecodeRow: Sized {
379    #[doc(hidden)]
380    fn decode_at(row: &PgRow, idx: &mut usize) -> sqlx::Result<Self>;
381}
382
383macro_rules! decode_row_leaf {
384    ($ty:ty) => {
385        impl DecodeRow for $ty {
386            fn decode_at(row: &PgRow, idx: &mut usize) -> sqlx::Result<Self> {
387                let v = row.try_get::<$ty, _>(*idx)?;
388                *idx += 1;
389                Ok(v)
390            }
391        }
392        impl DecodeRow for Option<$ty> {
393            fn decode_at(row: &PgRow, idx: &mut usize) -> sqlx::Result<Self> {
394                let v = row.try_get::<Option<$ty>, _>(*idx)?;
395                *idx += 1;
396                Ok(v)
397            }
398        }
399    };
400}
401decode_row_leaf!(i32);
402decode_row_leaf!(i64);
403decode_row_leaf!(f64);
404decode_row_leaf!(String);
405decode_row_leaf!(bool);
406decode_row_leaf!(Vec<u8>);
407#[cfg(feature = "chrono")]
408decode_row_leaf!(chrono::DateTime<chrono::Utc>);
409#[cfg(feature = "chrono")]
410decode_row_leaf!(chrono::NaiveDate);
411#[cfg(feature = "uuid")]
412decode_row_leaf!(uuid::Uuid);
413#[cfg(feature = "decimal")]
414decode_row_leaf!(rust_decimal::Decimal);
415
416impl DecodeRow for RowNil {
417    fn decode_at(_row: &PgRow, _idx: &mut usize) -> sqlx::Result<Self> {
418        Ok(RowNil)
419    }
420}
421
422impl<K, V: DecodeRow, Tail: DecodeRow> DecodeRow for RowCons<K, V, Tail> {
423    fn decode_at(row: &PgRow, idx: &mut usize) -> sqlx::Result<Self> {
424        let value = V::decode_at(row, idx)?;
425        Ok(RowCons::new(value, Tail::decode_at(row, idx)?))
426    }
427}
428
429impl<L: DecodeRow> DecodeRow for Row<L> {
430    fn decode_at(row: &PgRow, idx: &mut usize) -> sqlx::Result<Self> {
431        Ok(Row::new(L::decode_at(row, idx)?))
432    }
433}
434
435/// An erased query and a set-op chain were both rendered before their
436/// selection type was gone, leaving nothing for `Idx` to index — hence
437/// `RowQuery<()>`, the same trait with an empty proof.
438impl<Output: DecodeRow> RowQuery<()> for DynSelect<Postgres, Output> {
439    type Output = Output;
440    fn rendered(&self) -> (String, Vec<Value>) {
441        self.to_sql(Postgres)
442    }
443}
444
445impl<Output: DecodeRow> RowQuery<()> for SetOp<Postgres, Output> {
446    type Output = Output;
447    fn rendered(&self) -> (String, Vec<Value>) {
448        self.to_sql(Postgres)
449    }
450}
451
452/// Runs a `prepare!{}`-built query, resolving its named placeholders from
453/// `params` first. Separate from `LoadExt` only because the values arrive at
454/// the call rather than being baked into the query: one `Prepared` is meant
455/// to serve many calls, and `.resolve()` clones the template rather than
456/// re-rendering it.
457#[diagnostic::on_unimplemented(
458    message = "`{Self}` isn't a prepared query this crate can run",
459    label = "a `.prepare()`-built query is — `Prepared<D, Params, Output>`, params before output — and its `Params` have to be the ones it declared"
460)]
461pub trait PreparedQuery<Params> {
462    type Output: DecodeRow;
463    #[doc(hidden)]
464    fn resolved(&self, params: Params) -> Result<(String, Vec<Value>)>;
465}
466
467/// The bound is on the method, and the impls are per-builder, for the two
468/// reasons `LoadExt` explains.
469pub trait PreparedExt {
470    fn load<'e, Params, E: sqlx::PgExecutor<'e>>(
471        &self,
472        executor: E,
473        params: Params,
474    ) -> impl std::future::Future<Output = Result<Vec<<Self as PreparedQuery<Params>>::Output>>>
475    where
476        Self: PreparedQuery<Params>,
477    {
478        let resolved = self.resolved(params);
479        async move {
480            let (sql, values) = resolved?;
481            fetch_all::<<Self as PreparedQuery<Params>>::Output, E>(executor, &sql, values).await
482        }
483    }
484
485    fn load_one<'e, Params, E: sqlx::PgExecutor<'e>>(
486        &self,
487        executor: E,
488        params: Params,
489    ) -> impl std::future::Future<Output = Result<Option<<Self as PreparedQuery<Params>>::Output>>>
490    where
491        Self: PreparedQuery<Params>,
492    {
493        let resolved = self.resolved(params);
494        async move {
495            let (sql, values) = resolved?;
496            fetch_optional::<<Self as PreparedQuery<Params>>::Output, E>(executor, &sql, values)
497                .await
498        }
499    }
500}
501
502impl<D, Params, Output> PreparedExt for Prepared<D, Params, Output> {}
503
504// A prepared query's `load`/`count` are told apart from the plain ones by
505// arity, but `execute` is not — without this, it is the one terminal on the
506// one builder that reports nothing.
507impl<D, Params, Output> ExecuteExt for Prepared<D, Params, Output> {}
508
509#[diagnostic::do_not_recommend]
510impl<Params: PreparedParams, Output: DecodeRow> PreparedQuery<Params>
511    for Prepared<Postgres, Params, Output>
512{
513    type Output = Output;
514
515    fn resolved(&self, params: Params) -> Result<(String, Vec<Value>)> {
516        Ok(self.resolve(params)?)
517    }
518}
519
520/// A prepared total. Separate from `PreparedExt` for the reason `CountExt`
521/// is separate from `LoadExt`: a count produces a number, not rows.
522#[diagnostic::on_unimplemented(
523    message = "`{Self}` isn't a prepared total this crate can run",
524    label = "`.prepare_count()` builds one; `.prepare()` builds a query whose rows go through `.load(..)`"
525)]
526pub trait PreparedTotal<Params> {
527    #[doc(hidden)]
528    fn resolved_count(&self, params: Params) -> Result<(String, Vec<Value>)>;
529}
530
531impl<Params: PreparedParams> PreparedTotal<Params> for Prepared<Postgres, Params, Total> {
532    fn resolved_count(&self, params: Params) -> Result<(String, Vec<Value>)> {
533        Ok(self.resolve(params)?)
534    }
535}
536
537/// The bound is on the method, and the impls are per-builder, for the two
538/// reasons `LoadExt` explains.
539pub trait PreparedCountExt {
540    fn count<'e, Params, E: sqlx::PgExecutor<'e>>(
541        &self,
542        executor: E,
543        params: Params,
544    ) -> impl std::future::Future<Output = Result<i64>>
545    where
546        Self: PreparedTotal<Params>,
547    {
548        let resolved = self.resolved_count(params);
549        async move { count_rows(executor, resolved?).await }
550    }
551}
552
553impl<D, Params, Output> PreparedCountExt for Prepared<D, Params, Output> {}
554
555#[cfg(test)]
556mod tests {
557    use super::*;
558
559    // No real Postgres needed: binding inspects the `Value` enum before
560    // anything reaches the network, so an unresolved placeholder is
561    // reachable, and its error checkable, without a live DB.
562    #[test]
563    fn unresolved_placeholder_is_a_typed_error_not_a_sqlx_configuration_string() {
564        let query = sqlx::query(sqlx::AssertSqlSafe("SELECT $1"));
565        let err = match bind_all(query, vec![Value::Placeholder("email")]) {
566            Err(e) => e,
567            Ok(_) => panic!("unresolved placeholder must fail to bind"),
568        };
569
570        assert!(matches!(
571            err,
572            Error::UnresolvedPlaceholder(qbrs_core::select::UnresolvedPlaceholder("email"))
573        ));
574        // A real `std::error::Error`, so it composes with
575        // `anyhow`/`Box<dyn Error>`.
576        let _: &dyn std::error::Error = &err;
577        assert_eq!(err.to_string(), "no value provided for placeholder `email`");
578    }
579
580    #[test]
581    fn sqlx_errors_convert_via_from() {
582        let err: Error = sqlx::Error::RowNotFound.into();
583        assert!(matches!(err, Error::Sqlx(sqlx::Error::RowNotFound)));
584    }
585}