Skip to main content

es_entity/
one_time_executor.rs

1//! Type-safe wrapper to ensure one database operation per executor.
2//!
3//! [`OneTimeExecutor`] also implements [`sqlx::Executor`]: every statement
4//! executed through it is annotated with the active span's `traceparent` as a
5//! trailing SQL comment (see [`crate::sql_commenter`]). This is what allows
6//! statements observed server-side (`pg_stat_activity`, Postgres logs) to be
7//! matched to distributed traces.
8//!
9//! The annotation rewrites the statement *text* only; bind arguments and row
10//! mapping flow through unchanged, so it is transparent to `sqlx::query!`
11//! macro-generated queries as well as dynamically built ones.
12//!
13//! # Trade-off: annotated statements bypass the prepared statement cache
14//!
15//! The trace context makes each annotated statement's text unique, so it can
16//! never match sqlx's per-connection prepared statement cache. Annotated
17//! statements are therefore executed with `persistent(false)` (the unnamed
18//! statement) — bypassing the cache rather than thrashing it with single-use
19//! entries. The cost is a server-side parse + plan per annotated execution.
20//! Annotation only happens for *sampled* spans (see
21//! [`crate::sql_commenter::current_traceparent`]), so un-sampled traffic keeps
22//! full prepared-statement reuse.
23//!
24//! When there is no sampled span context the original query is passed through
25//! untouched and the executor adds no overhead.
26
27use async_stream::try_stream;
28use futures_core::stream::BoxStream;
29use futures_util::{TryStreamExt, future::BoxFuture};
30use sqlx::{Database, Describe, Error, Execute, Executor};
31
32use std::borrow::Cow;
33
34use crate::{db, operation::AtomicOperation, sql_commenter};
35
36/// A struct that owns an [`sqlx::Executor`].
37///
38/// Calling one of the `fetch_` `fn`s will consume it
39/// thus garuanteeing a 1 time usage.
40///
41/// It is not used directly but passed via the [`IntoOneTimeExecutor`] trait.
42///
43/// In order to make the consumption of the executor work we have to pass the query to the
44/// executor:
45/// ```rust
46/// async fn query(ex: impl es_entity::IntoOneTimeExecutor<'_>) -> Result<(), sqlx::Error> {
47///     ex.into_executor().fetch_optional(
48///         sqlx::query!(
49///             "SELECT NOW()"
50///         )
51///     ).await?;
52///     Ok(())
53/// }
54/// ```
55#[derive(Debug)]
56pub struct OneTimeExecutor<'c, E>
57where
58    E: sqlx::Executor<'c, Database = db::Db> + 'c,
59{
60    now: Option<chrono::DateTime<chrono::Utc>>,
61    executor: E,
62    _phantom: std::marker::PhantomData<&'c ()>,
63}
64
65impl<'c, E> OneTimeExecutor<'c, E>
66where
67    E: sqlx::Executor<'c, Database = db::Db> + 'c,
68{
69    pub(crate) fn new(executor: E, now: Option<chrono::DateTime<chrono::Utc>>) -> Self {
70        OneTimeExecutor {
71            executor,
72            now,
73            _phantom: std::marker::PhantomData,
74        }
75    }
76
77    pub fn maybe_now(&self) -> Option<chrono::DateTime<chrono::Utc>> {
78        self.now
79    }
80
81    /// Proxy call to `query.fetch_one` but guarantees the inner executor will only be used once.
82    pub async fn fetch_one<'q, F, O, A>(
83        self,
84        query: sqlx::query::Map<'q, db::Db, F, A>,
85    ) -> Result<O, sqlx::Error>
86    where
87        F: FnMut(db::Row) -> Result<O, sqlx::Error> + Send,
88        O: Send + Unpin,
89        A: 'q + Send + sqlx::IntoArguments<'q, db::Db>,
90    {
91        query.fetch_one(self).await
92    }
93
94    /// Proxy call to `query.fetch_all` but guarantees the inner executor will only be used once.
95    pub async fn fetch_all<'q, F, O, A>(
96        self,
97        query: sqlx::query::Map<'q, sqlx::Postgres, F, A>,
98    ) -> Result<Vec<O>, sqlx::Error>
99    where
100        F: FnMut(sqlx::postgres::PgRow) -> Result<O, sqlx::Error> + Send,
101        O: Send + Unpin,
102        A: 'q + Send + sqlx::IntoArguments<'q, sqlx::Postgres>,
103    {
104        query.fetch_all(self).await
105    }
106
107    /// Proxy call to `query.fetch_optional` but guarantees the inner executor will only be used once.
108    pub async fn fetch_optional<'q, F, O, A>(
109        self,
110        query: sqlx::query::Map<'q, sqlx::Postgres, F, A>,
111    ) -> Result<Option<O>, sqlx::Error>
112    where
113        F: FnMut(sqlx::postgres::PgRow) -> Result<O, sqlx::Error> + Send,
114        O: Send + Unpin,
115        A: 'q + Send + sqlx::IntoArguments<'q, sqlx::Postgres>,
116    {
117        query.fetch_optional(self).await
118    }
119}
120
121/// Returns the annotated statement text, or `None` when there is no sampled
122/// span context (in which case the query should be delegated untouched).
123fn annotated_sql<'q>(query: &impl Execute<'q, db::Db>) -> Option<String> {
124    match sql_commenter::annotate_sql(query.sql()) {
125        Cow::Borrowed(_) => None,
126        Cow::Owned(sql) => Some(sql),
127    }
128}
129
130/// Rebuilds a query with annotated SQL, moving out its bind arguments.
131///
132/// The trace context makes each statement's text unique, so the returned query
133/// is marked non-persistent: it can never hit sqlx's per-connection prepared
134/// statement cache, and caching it would evict useful entries. The trade-off
135/// is a server-side parse + plan per annotated execution.
136fn rebuild_annotated<'q>(
137    annotated: &str,
138    mut query: impl Execute<'q, db::Db>,
139) -> Result<sqlx::query::Query<'_, db::Db, sqlx::postgres::PgArguments>, Error> {
140    let args = query
141        .take_arguments()
142        .map_err(Error::Encode)?
143        .unwrap_or_default();
144    Ok(sqlx::query_with::<db::Db, _>(annotated, args).persistent(false))
145}
146
147impl<'c, E> Executor<'c> for OneTimeExecutor<'c, E>
148where
149    E: Executor<'c, Database = db::Db> + 'c,
150{
151    type Database = db::Db;
152
153    fn fetch_many<'e, 'q: 'e, Q>(
154        self,
155        query: Q,
156    ) -> BoxStream<'e, Result<sqlx::Either<<db::Db as Database>::QueryResult, db::Row>, Error>>
157    where
158        'c: 'e,
159        Q: 'q + Execute<'q, db::Db>,
160    {
161        let Some(annotated) = annotated_sql(&query) else {
162            return self.executor.fetch_many(query);
163        };
164        Box::pin(try_stream! {
165            let q = rebuild_annotated(annotated.as_str(), query)?;
166            let mut stream = self.executor.fetch_many(q);
167            while let Some(step) = stream.try_next().await? {
168                yield step;
169            }
170        })
171    }
172
173    fn fetch_optional<'e, 'q: 'e, Q>(
174        self,
175        query: Q,
176    ) -> BoxFuture<'e, Result<Option<db::Row>, Error>>
177    where
178        'c: 'e,
179        Q: 'q + Execute<'q, db::Db>,
180    {
181        let Some(annotated) = annotated_sql(&query) else {
182            return self.executor.fetch_optional(query);
183        };
184        Box::pin(async move {
185            let q = rebuild_annotated(annotated.as_str(), query)?;
186            self.executor.fetch_optional(q).await
187        })
188    }
189
190    fn prepare_with<'e, 'q: 'e>(
191        self,
192        sql: &'q str,
193        parameters: &'e [<db::Db as Database>::TypeInfo],
194    ) -> BoxFuture<'e, Result<<db::Db as Database>::Statement<'q>, Error>>
195    where
196        'c: 'e,
197    {
198        // A prepared Statement<'q> may borrow `sql`; a locally allocated
199        // annotated string could not satisfy the 'q lifetime, so preparation
200        // is delegated unannotated.
201        self.executor.prepare_with(sql, parameters)
202    }
203
204    fn describe<'e, 'q: 'e>(self, sql: &'q str) -> BoxFuture<'e, Result<Describe<db::Db>, Error>>
205    where
206        'c: 'e,
207    {
208        // Not an execution path; delegated unannotated.
209        self.executor.describe(sql)
210    }
211}
212
213/// Marker trait for [`IntoOneTimeExecutorAt<'a> + 'a`](`IntoOneTimeExecutorAt`). Do not implement directly.
214///
215/// Used as sugar to avoid writing:
216/// ```rust,ignore
217/// fn some_query<'a>(op: impl IntoOneTimeExecutorAt<'a> + 'a)
218/// ```
219/// Instead we can shorten the signature by using elision:
220/// ```rust,ignore
221/// fn some_query(op: impl IntoOneTimeExecutor<'_>)
222/// ```
223pub trait IntoOneTimeExecutor<'c>: IntoOneTimeExecutorAt<'c> + 'c {}
224impl<'c, T> IntoOneTimeExecutor<'c> for T where T: IntoOneTimeExecutorAt<'c> + 'c {}
225
226/// A trait to signify that we can use an argument for 1 round trip to the database
227///
228/// Auto implemented on all [`&mut AtomicOperation`](`AtomicOperation`) types and
229/// [`&db::Pool`](`crate::db::Pool`).
230pub trait IntoOneTimeExecutorAt<'c> {
231    /// The concrete executor type.
232    type Executor: sqlx::Executor<'c, Database = db::Db>;
233
234    /// Transforms into a [`OneTimeExecutor`] which can be used to execute a round trip.
235    fn into_executor(self) -> OneTimeExecutor<'c, Self::Executor>
236    where
237        Self: 'c;
238}
239
240impl<'c, E> IntoOneTimeExecutorAt<'c> for OneTimeExecutor<'c, E>
241where
242    E: sqlx::Executor<'c, Database = db::Db> + 'c,
243{
244    type Executor = E;
245
246    fn into_executor(self) -> OneTimeExecutor<'c, Self::Executor>
247    where
248        Self: 'c,
249    {
250        self
251    }
252}
253
254impl<'c> IntoOneTimeExecutorAt<'c> for &db::Pool {
255    type Executor = &'c db::Pool;
256
257    fn into_executor(self) -> OneTimeExecutor<'c, Self::Executor>
258    where
259        Self: 'c,
260    {
261        OneTimeExecutor::new(self, None)
262    }
263}
264
265impl<'c, O> IntoOneTimeExecutorAt<'c> for &mut O
266where
267    O: AtomicOperation,
268{
269    type Executor = &'c mut db::Connection;
270
271    fn into_executor(self) -> OneTimeExecutor<'c, Self::Executor>
272    where
273        Self: 'c,
274    {
275        let now = self.maybe_now();
276        OneTimeExecutor::new(self.connection(), now)
277    }
278}