Skip to main content

keelson_exec/
execute.rs

1use std::future::Future;
2
3use keelson_core::{FromValue, Query};
4
5use crate::error::ExecError;
6use crate::executor::{ExecResult, Executor, Statement};
7use crate::row::{FromRow, Row};
8
9/// The ergonomic verbs, hung on every [`Query`].
10///
11/// Blanket-implemented — `use keelson_exec::Execute` is the one import that
12/// makes `q.fetch_all(&db)` compile, and `db` is anything that implements
13/// [`Executor`]: a pool, a connection, a transaction, or a `&dyn Executor`.
14/// The methods build the query synchronously (the query knows its own
15/// dialect), so the returned future borrows only the executor.
16///
17/// This is also the funnel observability lives in (feature `tracing`): every
18/// verb passes through one pair of functions, so no backend can ship
19/// uninstrumented and no two backends can drift. Calling [`Executor::fetch`]
20/// directly bypasses the sugar and the spans together; that path is the
21/// escape hatch and is documented as such.
22pub trait Execute: Query {
23    /// Every row, mapped to `T`.
24    fn fetch_all<T: FromRow>(
25        &self,
26        db: &(impl Executor + ?Sized),
27    ) -> impl Future<Output = Result<Vec<T>, ExecError>> + Send {
28        let stmt = Statement::from_query(self);
29        async move {
30            let rows = run_fetch(db, stmt?).await?;
31            rows.into_iter().map(|mut r| T::from_row(&mut r)).collect()
32        }
33    }
34
35    /// Every row, undecoded.
36    ///
37    /// The row-mapper seam Layer 2 needs: a model query decodes the base
38    /// struct first and then lets its preload mapper mods take the prefixed
39    /// relation columns out of the *same* row, so the rows must come back as
40    /// [`Row`]s without an intermediate decode. (`fetch_all::<Row>` would work
41    /// but clones every row on the way through `FromRow`.) Same funnel, same
42    /// tracing, as every other verb.
43    fn fetch_rows(
44        &self,
45        db: &(impl Executor + ?Sized),
46    ) -> impl Future<Output = Result<Vec<Row>, ExecError>> + Send {
47        let stmt = Statement::from_query(self);
48        async move { run_fetch(db, stmt?).await }
49    }
50
51    /// Exactly one row. Zero rows is [`ExecError::RowNotFound`]; a second row
52    /// is [`ExecError::TooManyRows`] — "one" means one.
53    fn fetch_one<T: FromRow>(
54        &self,
55        db: &(impl Executor + ?Sized),
56    ) -> impl Future<Output = Result<T, ExecError>> + Send {
57        let stmt = Statement::from_query(self);
58        async move {
59            let mut rows = run_fetch(db, stmt?).await?;
60            match rows.len() {
61                0 => Err(ExecError::RowNotFound),
62                1 => T::from_row(&mut rows[0]),
63                _ => Err(ExecError::TooManyRows),
64            }
65        }
66    }
67
68    /// At most one row. A second row is still [`ExecError::TooManyRows`].
69    fn fetch_optional<T: FromRow>(
70        &self,
71        db: &(impl Executor + ?Sized),
72    ) -> impl Future<Output = Result<Option<T>, ExecError>> + Send {
73        let stmt = Statement::from_query(self);
74        async move {
75            let mut rows = run_fetch(db, stmt?).await?;
76            match rows.len() {
77                0 => Ok(None),
78                1 => T::from_row(&mut rows[0]).map(Some),
79                _ => Err(ExecError::TooManyRows),
80            }
81        }
82    }
83
84    /// The first column of the single row — `SELECT count(*)`, or an
85    /// `INSERT … RETURNING id`.
86    ///
87    /// A separate verb rather than a blanket `FromRow for T: FromValue`,
88    /// which would collide with a type implementing both; the verb is clearer
89    /// at the call site anyway.
90    fn fetch_scalar<T: FromValue>(
91        &self,
92        db: &(impl Executor + ?Sized),
93    ) -> impl Future<Output = Result<T, ExecError>> + Send {
94        let stmt = Statement::from_query(self);
95        async move {
96            let mut rows = run_fetch(db, stmt?).await?;
97            match rows.len() {
98                0 => Err(ExecError::RowNotFound),
99                1 => rows[0].take_at(0),
100                _ => Err(ExecError::TooManyRows),
101            }
102        }
103    }
104
105    /// The first column of every row.
106    fn fetch_scalars<T: FromValue>(
107        &self,
108        db: &(impl Executor + ?Sized),
109    ) -> impl Future<Output = Result<Vec<T>, ExecError>> + Send {
110        let stmt = Statement::from_query(self);
111        async move {
112            let rows = run_fetch(db, stmt?).await?;
113            rows.into_iter().map(|mut r| r.take_at(0)).collect()
114        }
115    }
116
117    /// Run for the side effect.
118    fn execute(
119        &self,
120        db: &(impl Executor + ?Sized),
121    ) -> impl Future<Output = Result<ExecResult, ExecError>> + Send {
122        let stmt = Statement::from_query(self);
123        async move { run_execute(db, stmt?).await }
124    }
125}
126
127impl<Q: Query + ?Sized> Execute for Q {}
128
129/// The one place a row-returning statement passes on its way to a backend.
130pub(crate) async fn run_fetch<E: Executor + ?Sized>(
131    db: &E,
132    stmt: Statement,
133) -> Result<Vec<Row>, ExecError> {
134    #[cfg(feature = "tracing")]
135    {
136        use tracing::Instrument as _;
137        let span = query_span(db, &stmt);
138        // Recorded on the handle, not on `Span::current()`: the latter needs
139        // the subscriber to track span entry, which not every subscriber does.
140        let res = db.fetch(stmt).instrument(span.clone()).await;
141        match &res {
142            Ok(rows) => span.record("keelson.rows", rows.len() as u64),
143            Err(e) => span.record("error", tracing::field::display(e)),
144        };
145        res
146    }
147    #[cfg(not(feature = "tracing"))]
148    {
149        db.fetch(stmt).await
150    }
151}
152
153/// The one place a side-effect statement passes on its way to a backend.
154pub(crate) async fn run_execute<E: Executor + ?Sized>(
155    db: &E,
156    stmt: Statement,
157) -> Result<ExecResult, ExecError> {
158    #[cfg(feature = "tracing")]
159    {
160        use tracing::Instrument as _;
161        let span = query_span(db, &stmt);
162        let res = db.execute(stmt).instrument(span.clone()).await;
163        match &res {
164            Ok(done) => span.record("keelson.rows_affected", done.rows_affected),
165            Err(e) => span.record("error", tracing::field::display(e)),
166        };
167        res
168    }
169    #[cfg(not(feature = "tracing"))]
170    {
171        db.execute(stmt).await
172    }
173}
174
175/// The per-statement span. Field names follow the OTel database semconv so
176/// existing dashboards light up.
177///
178/// `db.query.text` is the full SQL, untruncated — it is parameterized text and
179/// safe by construction (placeholders, never values; the sole way user data
180/// reaches SQL text is `expr::literal`, documented there), and a truncated
181/// query is the one you cannot paste into `EXPLAIN`. The *arguments* are never
182/// recorded, at any level, on any field: they are the PII channel. Only their
183/// count is, so "did the IN-list explode" stays answerable. Pinned by test in
184/// `tests/tracing.rs`.
185#[cfg(feature = "tracing")]
186fn query_span<E: Executor + ?Sized>(db: &E, stmt: &Statement) -> tracing::Span {
187    tracing::info_span!(
188        "keelson.query",
189        db.system = db.family().as_str(),
190        db.query.text = %stmt.sql,
191        keelson.query_type = %stmt.query_type,
192        keelson.args.count = stmt.args.len() as u64,
193        keelson.rows = tracing::field::Empty,
194        keelson.rows_affected = tracing::field::Empty,
195        error = tracing::field::Empty,
196    )
197}