sqlx_otel/query_ext.rs
1//! Query-side annotation surface – mirror of the executor-side
2//! [`with_annotations`](crate::Pool::with_annotations) /
3//! [`with_operation`](crate::Pool::with_operation) methods, but attached to the query
4//! builder produced by [`sqlx::query()`], [`sqlx::query_as()`], and
5//! [`sqlx::query_scalar()`].
6//!
7//! # Choosing between executor-side and query-side
8//!
9//! The two surfaces emit identical telemetry; pick whichever keeps the annotation closer to
10//! the thing it describes. As a rule of thumb:
11//!
12//! - **Query-side** is the default. Per-query attributes describe the *query*, and
13//! colocating them with the query text keeps both readable.
14//! - **Executor-side** is for cases where one annotation set is reused across many queries
15//! on the same executor (e.g. a request-scoped logical operation), or where the surface
16//! is `prepare` / `describe` rather than `execute` / `fetch*`.
17//!
18//! ```no_run
19//! # #[cfg(feature = "sqlite")]
20//! # async fn _doc() -> Result<(), sqlx::Error> {
21//! # use sqlx_otel::PoolBuilder;
22//! use sqlx_otel::{QueryAnnotateExt, QueryAnnotations};
23//! # let pool = PoolBuilder::from(sqlx::SqlitePool::connect(":memory:").await?).build();
24//!
25//! sqlx::query("SELECT * FROM users WHERE id = ?")
26//! .bind(42_i64)
27//! .with_annotations(
28//! QueryAnnotations::new()
29//! .operation("SELECT")
30//! .collection("users"),
31//! )
32//! .execute(&pool)
33//! .await?;
34//! # Ok(())
35//! # }
36//! ```
37//!
38//! The wrapper [`AnnotatedQuery`] exposes the same `execute`/`fetch*` surface as the inner
39//! query and threads the annotations into span creation by wrapping the executor with the
40//! existing [`Annotated`](Annotated) / [`AnnotatedMut`](AnnotatedMut) executor wrappers.
41//!
42//! # Map and macro queries
43//!
44//! [`Query::map`](Query::map) / [`Query::try_map`](Query::try_map) return
45//! [`sqlx::query::Map<'q, DB, F, A>`](sqlx::query::Map), which is also covered by the
46//! trait. `with_annotations` and `with_operation` may be applied at any of the three
47//! positions on a hand-written `Query::map()` chain – before `bind`, between `bind` and
48//! `map`, or after `map`:
49//!
50//! ```no_run
51//! # #[cfg(feature = "sqlite")]
52//! # async fn _doc() -> Result<(), sqlx::Error> {
53//! # use sqlx_otel::PoolBuilder;
54//! use sqlx::Row as _;
55//! use sqlx_otel::QueryAnnotateExt;
56//! # let pool = PoolBuilder::from(sqlx::SqlitePool::connect(":memory:").await?).build();
57//!
58//! sqlx::query("SELECT id FROM users WHERE name = ?")
59//! .bind("alice")
60//! .map(|row: sqlx::sqlite::SqliteRow| row.get::<i64, _>("id"))
61//! .with_operation("SELECT", "users")
62//! .fetch_one(&pool)
63//! .await?;
64//! # Ok(())
65//! # }
66//! ```
67//!
68//! The compile-time validated macro forms (`sqlx::query!()`, `sqlx::query_as!()`,
69//! `sqlx::query_scalar!()`) expand to either `Query<'q, DB, _>` (for no-result-column
70//! shapes) or `Map<'q, DB, _, _>` (for any shape that decodes columns). Both are covered:
71//!
72//! ```ignore
73//! sqlx::query_as!(User, "SELECT id, name FROM users WHERE id = ?", 42_i64)
74//! .with_operation("SELECT", "users")
75//! .fetch_one(&pool)
76//! .await?;
77//! ```
78//!
79//! Macro queries can only carry annotations *after* the macro returns – the macro itself
80//! pre-applies `bind` and `try_map`, so positions 1 and 2 are not reachable by the user.
81//! The macro example above stays `ignore` because it requires either an offline `.sqlx/`
82//! cache or a live `DATABASE_URL`.
83
84use futures::stream::BoxStream;
85use sqlx::query::{Map, Query, QueryAs, QueryScalar};
86
87use crate::annotations::{Annotated, AnnotatedMut, QueryAnnotations};
88use crate::database::Database;
89
90/// Sealing module for [`QueryAnnotateExt`]. Downstream crates cannot implement the trait
91/// because the wrapper types it produces (`Annotated` / `AnnotatedMut`) have private fields
92/// that only this crate can construct.
93mod sealed {
94 /// Marker trait that prevents external impls of [`super::QueryAnnotateExt`].
95 pub trait Sealed {}
96}
97
98/// Extension trait that attaches OpenTelemetry per-query annotations to the function-form
99/// `SQLx` query builders ([`sqlx::query()`], [`sqlx::query_as()`],
100/// [`sqlx::query_scalar()`]) and to the [`Map`](sqlx::query::Map) returned by
101/// `Query::map` / `Query::try_map`.
102///
103/// The trait is sealed and cannot be implemented downstream.
104///
105/// # Example
106///
107/// ```no_run
108/// # #[cfg(feature = "sqlite")]
109/// # async fn _doc() -> Result<(), sqlx::Error> {
110/// # use sqlx_otel::PoolBuilder;
111/// use sqlx_otel::QueryAnnotateExt;
112/// # let pool = PoolBuilder::from(sqlx::SqlitePool::connect(":memory:").await?).build();
113///
114/// sqlx::query("INSERT INTO orders (user_id) VALUES (?)")
115/// .bind(42_i64)
116/// .with_operation("INSERT", "orders")
117/// .execute(&pool)
118/// .await?;
119/// # Ok(())
120/// # }
121/// ```
122pub trait QueryAnnotateExt: sealed::Sealed + Sized {
123 /// Wrap the query with the given per-query annotations.
124 ///
125 /// Returns an [`AnnotatedQuery`] that exposes the same `execute`/`fetch*` surface as the
126 /// inner query but threads the annotations through to OpenTelemetry span creation.
127 fn with_annotations(self, annotations: QueryAnnotations) -> AnnotatedQuery<Self>;
128
129 /// Shorthand that sets `db.operation.name` and `db.collection.name`.
130 ///
131 /// Equivalent to
132 /// `self.with_annotations(QueryAnnotations::new().operation(op).collection(coll))`.
133 ///
134 /// The returned future is `Send` and may be used inside a `Send`-required async context
135 /// (e.g. `tokio::spawn`, axum handlers, `tower::Service`-bounded futures). The example
136 /// below intentionally swallows the inner `sqlx::Error` via `.ok().flatten()` – the
137 /// point is to exercise the `Send` contract on the spawned future at compile time, not
138 /// to demonstrate error handling. A non-`Send` future would fail to compile here, since
139 /// `tokio::spawn` requires `Send`.
140 ///
141 /// ```no_run
142 /// # #[cfg(feature = "sqlite")]
143 /// # async fn _doc() -> Result<(), Box<dyn std::error::Error>> {
144 /// # use sqlx_otel::PoolBuilder;
145 /// use sqlx_otel::QueryAnnotateExt as _;
146 /// # let pool: sqlx_otel::Pool<sqlx::Sqlite> =
147 /// # PoolBuilder::from(sqlx::SqlitePool::connect(":memory:").await?).build();
148 ///
149 /// tokio::spawn(async move {
150 /// let _: Option<(i32,)> = sqlx::query_as("SELECT 1")
151 /// .with_operation("SELECT", "users")
152 /// .fetch_optional(&pool)
153 /// .await
154 /// .ok()
155 /// .flatten();
156 /// })
157 /// .await?;
158 /// # Ok(()) }
159 /// ```
160 fn with_operation(
161 self,
162 operation: impl Into<String>,
163 collection: impl Into<String>,
164 ) -> AnnotatedQuery<Self> {
165 self.with_annotations(
166 QueryAnnotations::new()
167 .operation(operation)
168 .collection(collection),
169 )
170 }
171}
172
173impl<DB: sqlx::Database, A> sealed::Sealed for Query<'_, DB, A> {}
174impl<DB: sqlx::Database, A> QueryAnnotateExt for Query<'_, DB, A> {
175 fn with_annotations(self, annotations: QueryAnnotations) -> AnnotatedQuery<Self> {
176 AnnotatedQuery {
177 inner: self,
178 annotations,
179 }
180 }
181}
182
183impl<DB: sqlx::Database, O, A> sealed::Sealed for QueryAs<'_, DB, O, A> {}
184impl<DB: sqlx::Database, O, A> QueryAnnotateExt for QueryAs<'_, DB, O, A> {
185 fn with_annotations(self, annotations: QueryAnnotations) -> AnnotatedQuery<Self> {
186 AnnotatedQuery {
187 inner: self,
188 annotations,
189 }
190 }
191}
192
193impl<DB: sqlx::Database, O, A> sealed::Sealed for QueryScalar<'_, DB, O, A> {}
194impl<DB: sqlx::Database, O, A> QueryAnnotateExt for QueryScalar<'_, DB, O, A> {
195 fn with_annotations(self, annotations: QueryAnnotations) -> AnnotatedQuery<Self> {
196 AnnotatedQuery {
197 inner: self,
198 annotations,
199 }
200 }
201}
202
203impl<DB: sqlx::Database, F, A> sealed::Sealed for Map<'_, DB, F, A> {}
204impl<DB: sqlx::Database, F, A> QueryAnnotateExt for Map<'_, DB, F, A> {
205 fn with_annotations(self, annotations: QueryAnnotations) -> AnnotatedQuery<Self> {
206 AnnotatedQuery {
207 inner: self,
208 annotations,
209 }
210 }
211}
212
213/// A `SQLx` query builder paired with OpenTelemetry per-query annotations.
214///
215/// Produced by [`QueryAnnotateExt::with_annotations`] / [`QueryAnnotateExt::with_operation`].
216/// Each invocation of the executor-driven methods (`execute`, `fetch_all`, etc.) wraps the
217/// executor with the annotations so the resulting span carries them.
218///
219/// The wrapper exposes [`bind`](AnnotatedQuery::bind) so the caller can chain
220/// `.bind(...).with_annotations(...)` or `.with_annotations(...).bind(...)` interchangeably.
221#[must_use = "annotated queries do nothing until you call execute / fetch* on them"]
222pub struct AnnotatedQuery<Q> {
223 inner: Q,
224 annotations: QueryAnnotations,
225}
226
227impl<Q> std::fmt::Debug for AnnotatedQuery<Q> {
228 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
229 f.debug_struct("AnnotatedQuery")
230 .field("annotations", &self.annotations)
231 .finish_non_exhaustive()
232 }
233}
234
235// ---------------------------------------------------------------------------
236// Internal trait: convert an executor argument into the existing Annotated /
237// AnnotatedMut wrappers. Implemented for the same three borrow shapes that the
238// `impl_executor!` macro covers.
239// ---------------------------------------------------------------------------
240
241/// Internal conversion trait. Implemented for the three executor borrow shapes that
242/// `impl_executor!` already supports (`&Pool`, `&mut PoolConnection`, `&mut Transaction`),
243/// each producing the matching [`Annotated`] / [`AnnotatedMut`] wrapper.
244///
245/// The HRTB `for<'a> &'a mut DB::Connection: sqlx::Executor<'a, Database = DB>` lives on each
246/// individual impl rather than on the trait, to avoid trait-resolution recursion when the
247/// user's executor type is itself constructed via the same HRTB. This bound used to leak into
248/// the `Send` auto-trait inference of `AnnotatedQuery::fetch_*` / `execute` when those methods
249/// were `async fn` – the resulting opaque coroutine carried a region-quantified obligation
250/// that rustc could not always discharge, which broke query-side annotation inside contexts
251/// requiring `Send` (`tokio::spawn`, axum handlers). The fix is in the *callers* of this
252/// trait, not the trait itself: the forwarders on `AnnotatedQuery` are written as
253/// `fn(...) -> impl Future + Send + 'e` returning `SQLx`'s own future directly, so no
254/// coroutine forms on this crate's side and the `Send` check delegates to the underlying
255/// `SQLx` future, which is already `Send`-clean for concrete backends.
256///
257/// Users do not call this trait directly – they pass `&pool`, `&mut conn`, or `&mut tx` to
258/// [`AnnotatedQuery::execute`] / `fetch*` and the trait dispatches internally. The trait is
259/// effectively sealed because only this crate can construct the [`Annotated`] /
260/// [`AnnotatedMut`] wrapper types (their fields are crate-private).
261#[doc(hidden)]
262pub trait IntoAnnotatedExecutor<'e, DB: Database> {
263 /// The annotated executor wrapper type produced by [`into_annotated`](Self::into_annotated).
264 type Wrapper: sqlx::Executor<'e, Database = DB>;
265
266 /// Consume `self` together with the per-query annotations to produce the wrapper.
267 fn into_annotated(self, annotations: QueryAnnotations) -> Self::Wrapper;
268}
269
270impl<'e, DB> IntoAnnotatedExecutor<'e, DB> for &'e crate::Pool<DB>
271where
272 DB: Database,
273 for<'a> &'a mut DB::Connection: sqlx::Executor<'a, Database = DB>,
274{
275 type Wrapper = Annotated<'e, crate::Pool<DB>>;
276
277 fn into_annotated(self, annotations: QueryAnnotations) -> Self::Wrapper {
278 Annotated {
279 inner: self,
280 annotations,
281 state: self.state.clone(),
282 }
283 }
284}
285
286impl<'e, DB> IntoAnnotatedExecutor<'e, DB> for &'e mut crate::PoolConnection<DB>
287where
288 DB: Database,
289 for<'a> &'a mut DB::Connection: sqlx::Executor<'a, Database = DB>,
290{
291 type Wrapper = AnnotatedMut<'e, crate::PoolConnection<DB>>;
292
293 fn into_annotated(self, annotations: QueryAnnotations) -> Self::Wrapper {
294 AnnotatedMut {
295 state: self.state.clone(),
296 annotations,
297 inner: self,
298 }
299 }
300}
301
302impl<'e, 'tx, DB> IntoAnnotatedExecutor<'e, DB> for &'e mut crate::Transaction<'tx, DB>
303where
304 DB: Database,
305 for<'a> &'a mut DB::Connection: sqlx::Executor<'a, Database = DB>,
306{
307 type Wrapper = AnnotatedMut<'e, crate::Transaction<'tx, DB>>;
308
309 fn into_annotated(self, annotations: QueryAnnotations) -> Self::Wrapper {
310 AnnotatedMut {
311 state: self.state.clone(),
312 annotations,
313 inner: self,
314 }
315 }
316}
317
318// ---------------------------------------------------------------------------
319// Forwarder macros: emit method bodies inside a caller-supplied `impl` block.
320//
321// The macros expand to method items only (no `impl` header), so each call site
322// declares its own generics and where-clauses. This avoids the local-ambiguity
323// that arises when the macro tries to consume an `impl<...>` header via `tt`
324// repetition, while still removing the per-builder duplication.
325//
326// Symbols referenced in the bodies (`Self`, `DB`, `A`, `O`, `QueryAnnotations`,
327// `IntoAnnotatedExecutor`, `BoxStream`) resolve in the expansion context.
328// ---------------------------------------------------------------------------
329
330/// Emit the methods that every `AnnotatedQuery<Inner>` impl block has in common:
331/// `with_annotations`, `with_operation`, and the five `fetch*` forwarders.
332///
333/// Parameters:
334/// * `row` – the row type of the inner builder (`DB::Row` for `Query`, `O` for
335/// `QueryAs` and `QueryScalar`).
336/// * `extra_bounds` – additional lifetime bounds threaded onto each fetch-forwarder's
337/// where-clause (`DB: 'e, O: 'e,` for `QueryAs` / `QueryScalar`; empty for `Query`).
338macro_rules! impl_annotated_query_fetch_forwarders {
339 (row = $row:ty, extra_bounds = ($($extra_bounds:tt)*)) => {
340 /// Replace the wrapper's annotations. Last-call-wins – any previously set annotations
341 /// on this wrapper are discarded.
342 pub fn with_annotations(mut self, annotations: QueryAnnotations) -> Self {
343 self.annotations = annotations;
344 self
345 }
346
347 /// Shorthand replacement that sets `db.operation.name` and `db.collection.name`.
348 /// Discards any previously set annotations on this wrapper.
349 pub fn with_operation(
350 self,
351 operation: impl Into<String>,
352 collection: impl Into<String>,
353 ) -> Self {
354 self.with_annotations(
355 QueryAnnotations::new()
356 .operation(operation)
357 .collection(collection),
358 )
359 }
360
361 /// Stream the resulting rows.
362 pub fn fetch<'e, E>(self, executor: E) -> BoxStream<'e, Result<$row, sqlx::Error>>
363 where
364 'q: 'e,
365 A: 'e,
366 $($extra_bounds)*
367 E: 'e + IntoAnnotatedExecutor<'e, DB>,
368 {
369 let wrapper = executor.into_annotated(self.annotations);
370 self.inner.fetch(wrapper)
371 }
372
373 /// Stream a mix of `QueryResult`s and rows for multi-statement queries.
374 ///
375 /// `fetch_many` is `#[deprecated]` in `SQLx` 0.8 but kept for parity with the
376 /// executor-side surface.
377 #[allow(deprecated, clippy::type_complexity)]
378 pub fn fetch_many<'e, E>(
379 self,
380 executor: E,
381 ) -> BoxStream<'e, Result<sqlx::Either<DB::QueryResult, $row>, sqlx::Error>>
382 where
383 'q: 'e,
384 A: 'e,
385 $($extra_bounds)*
386 E: 'e + IntoAnnotatedExecutor<'e, DB>,
387 {
388 let wrapper = executor.into_annotated(self.annotations);
389 self.inner.fetch_many(wrapper)
390 }
391
392 /// Collect every row into a `Vec`.
393 ///
394 /// # Errors
395 ///
396 /// Returns any [`sqlx::Error`] surfaced by the underlying driver, including row
397 /// decoding errors.
398 pub fn fetch_all<'e, E>(
399 self,
400 executor: E,
401 ) -> impl 'e + Send + std::future::Future<Output = Result<Vec<$row>, sqlx::Error>>
402 where
403 'q: 'e,
404 A: 'e,
405 $($extra_bounds)*
406 E: 'e + IntoAnnotatedExecutor<'e, DB>,
407 {
408 let wrapper = executor.into_annotated(self.annotations);
409 self.inner.fetch_all(wrapper)
410 }
411
412 /// Return exactly one row, erroring if none or more than one.
413 ///
414 /// # Errors
415 ///
416 /// Returns [`sqlx::Error::RowNotFound`] when the result set is empty, or any other
417 /// [`sqlx::Error`] surfaced by the underlying driver.
418 pub fn fetch_one<'e, E>(
419 self,
420 executor: E,
421 ) -> impl 'e + Send + std::future::Future<Output = Result<$row, sqlx::Error>>
422 where
423 'q: 'e,
424 A: 'e,
425 $($extra_bounds)*
426 E: 'e + IntoAnnotatedExecutor<'e, DB>,
427 {
428 let wrapper = executor.into_annotated(self.annotations);
429 self.inner.fetch_one(wrapper)
430 }
431
432 /// Return at most one row.
433 ///
434 /// # Errors
435 ///
436 /// Returns any [`sqlx::Error`] surfaced by the underlying driver.
437 pub fn fetch_optional<'e, E>(
438 self,
439 executor: E,
440 ) -> impl 'e + Send + std::future::Future<Output = Result<Option<$row>, sqlx::Error>>
441 where
442 'q: 'e,
443 A: 'e,
444 $($extra_bounds)*
445 E: 'e + IntoAnnotatedExecutor<'e, DB>,
446 {
447 let wrapper = executor.into_annotated(self.annotations);
448 self.inner.fetch_optional(wrapper)
449 }
450 };
451}
452
453/// Emit the `bind` method on a specialised impl block.
454///
455/// `SQLx` restricts `bind` to the default `<DB as Database>::Arguments<'q>` parameter, so
456/// each builder family has its own dedicated impl block keyed on the default arguments.
457macro_rules! impl_annotated_query_bind {
458 () => {
459 /// Append a parameter binding, forwarding to the inner query. Mirrors the inner
460 /// builder's own `bind` method.
461 pub fn bind<T>(mut self, value: T) -> Self
462 where
463 T: 'q + sqlx::Encode<'q, DB> + sqlx::Type<DB>,
464 {
465 self.inner = self.inner.bind(value);
466 self
467 }
468 };
469}
470
471// --- AnnotatedQuery<Query<'q, DB, A>> --------------------------------------
472
473impl<'q, DB, A> AnnotatedQuery<Query<'q, DB, A>>
474where
475 DB: Database,
476 A: 'q + Send + sqlx::IntoArguments<'q, DB>,
477{
478 impl_annotated_query_fetch_forwarders!(row = DB::Row, extra_bounds = ());
479
480 /// Execute the query and return the number of rows affected. Wraps the executor with
481 /// the carried annotations so the resulting span is annotated.
482 ///
483 /// # Errors
484 ///
485 /// Returns any [`sqlx::Error`] surfaced by the underlying driver.
486 pub fn execute<'e, E>(
487 self,
488 executor: E,
489 ) -> impl 'e + Send + std::future::Future<Output = Result<DB::QueryResult, sqlx::Error>>
490 where
491 'q: 'e,
492 A: 'e,
493 E: 'e + IntoAnnotatedExecutor<'e, DB>,
494 {
495 let wrapper = executor.into_annotated(self.annotations);
496 self.inner.execute(wrapper)
497 }
498
499 /// Execute multiple statements separated by `;` and return their results as a stream.
500 ///
501 /// `execute_many` is `#[deprecated]` in `SQLx` 0.8 but kept here for parity with the
502 /// existing executor-side surface. Only `Query` exposes this method – `QueryAs`,
503 /// `QueryScalar`, and `Map` have no `execute_many` upstream.
504 #[allow(deprecated)]
505 pub fn execute_many<'e, E>(
506 self,
507 executor: E,
508 ) -> impl 'e + Send + std::future::Future<Output = BoxStream<'e, Result<DB::QueryResult, sqlx::Error>>>
509 where
510 'q: 'e,
511 A: 'e,
512 E: 'e + IntoAnnotatedExecutor<'e, DB>,
513 {
514 let wrapper = executor.into_annotated(self.annotations);
515 self.inner.execute_many(wrapper)
516 }
517
518 /// Map each row to another type. Mirrors [`sqlx::query::Query::map`] and carries the
519 /// existing annotations forward unchanged onto the resulting `AnnotatedQuery<Map<...>>`,
520 /// so `with_annotations` can be applied either before or after `.map()`.
521 #[allow(clippy::type_complexity)]
522 pub fn map<F, O>(
523 self,
524 f: F,
525 ) -> AnnotatedQuery<Map<'q, DB, impl FnMut(DB::Row) -> Result<O, sqlx::Error> + Send, A>>
526 where
527 F: FnMut(DB::Row) -> O + Send,
528 O: Unpin,
529 {
530 AnnotatedQuery {
531 inner: self.inner.map(f),
532 annotations: self.annotations,
533 }
534 }
535
536 /// Map each row to a `Result`. Mirrors [`sqlx::query::Query::try_map`] and carries the
537 /// existing annotations forward unchanged.
538 pub fn try_map<F, O>(self, f: F) -> AnnotatedQuery<Map<'q, DB, F, A>>
539 where
540 F: FnMut(DB::Row) -> Result<O, sqlx::Error> + Send,
541 O: Unpin,
542 {
543 AnnotatedQuery {
544 inner: self.inner.try_map(f),
545 annotations: self.annotations,
546 }
547 }
548}
549
550impl<'q, DB> AnnotatedQuery<Query<'q, DB, <DB as sqlx::Database>::Arguments<'q>>>
551where
552 DB: sqlx::Database,
553{
554 impl_annotated_query_bind!();
555}
556
557// --- AnnotatedQuery<QueryAs<'q, DB, O, A>> ---------------------------------
558
559impl<'q, DB, O, A> AnnotatedQuery<QueryAs<'q, DB, O, A>>
560where
561 DB: Database,
562 A: 'q + Send + sqlx::IntoArguments<'q, DB>,
563 O: Send + Unpin + for<'r> sqlx::FromRow<'r, DB::Row>,
564{
565 impl_annotated_query_fetch_forwarders!(row = O, extra_bounds = (DB: 'e, O: 'e,));
566}
567
568impl<'q, DB, O> AnnotatedQuery<QueryAs<'q, DB, O, <DB as sqlx::Database>::Arguments<'q>>>
569where
570 DB: sqlx::Database,
571{
572 impl_annotated_query_bind!();
573}
574
575// --- AnnotatedQuery<QueryScalar<'q, DB, O, A>> -----------------------------
576
577impl<'q, DB, O, A> AnnotatedQuery<QueryScalar<'q, DB, O, A>>
578where
579 DB: Database,
580 A: 'q + Send + sqlx::IntoArguments<'q, DB>,
581 O: Send + Unpin,
582 (O,): Send + Unpin + for<'r> sqlx::FromRow<'r, DB::Row>,
583{
584 impl_annotated_query_fetch_forwarders!(row = O, extra_bounds = (DB: 'e, O: 'e,));
585}
586
587impl<'q, DB, O> AnnotatedQuery<QueryScalar<'q, DB, O, <DB as sqlx::Database>::Arguments<'q>>>
588where
589 DB: sqlx::Database,
590{
591 impl_annotated_query_bind!();
592}
593
594// --- AnnotatedQuery<Map<'q, DB, F, A>> -------------------------------------
595
596impl<'q, DB, F, A, O> AnnotatedQuery<Map<'q, DB, F, A>>
597where
598 DB: Database,
599 F: FnMut(DB::Row) -> Result<O, sqlx::Error> + Send,
600 O: Send + Unpin,
601 A: 'q + Send + sqlx::IntoArguments<'q, DB>,
602{
603 impl_annotated_query_fetch_forwarders!(row = O, extra_bounds = (DB: 'e, F: 'e, O: 'e,));
604
605 /// Compose a further mapping on top of this annotated map. Mirrors
606 /// [`sqlx::query::Map::map`] (which itself composes via `f(row).and_then(&mut g)`) and
607 /// preserves the existing annotations on the wrapper.
608 #[allow(clippy::type_complexity)]
609 pub fn map<G, P>(
610 self,
611 g: G,
612 ) -> AnnotatedQuery<Map<'q, DB, impl FnMut(DB::Row) -> Result<P, sqlx::Error> + Send, A>>
613 where
614 G: FnMut(O) -> P + Send,
615 P: Unpin,
616 {
617 AnnotatedQuery {
618 inner: self.inner.map(g),
619 annotations: self.annotations,
620 }
621 }
622
623 /// Fallible variant of [`map`](Self::map). Mirrors [`sqlx::query::Map::try_map`] and
624 /// preserves the existing annotations on the wrapper.
625 #[allow(clippy::type_complexity)]
626 pub fn try_map<G, P>(
627 self,
628 g: G,
629 ) -> AnnotatedQuery<Map<'q, DB, impl FnMut(DB::Row) -> Result<P, sqlx::Error> + Send, A>>
630 where
631 G: FnMut(O) -> Result<P, sqlx::Error> + Send,
632 P: Unpin,
633 {
634 AnnotatedQuery {
635 inner: self.inner.try_map(g),
636 annotations: self.annotations,
637 }
638 }
639}
640
641#[cfg(all(test, feature = "sqlite"))]
642mod tests {
643 use sqlx::Execute as _;
644 use sqlx::Sqlite;
645
646 use super::*;
647
648 // --- query() / query_as() / query_scalar() --------------------
649
650 #[test]
651 fn with_annotations_replaces_previous() {
652 let q = sqlx::query::<Sqlite>("SELECT 1")
653 .with_annotations(QueryAnnotations::new().operation("FIRST"))
654 .with_annotations(QueryAnnotations::new().operation("SECOND"));
655 assert_eq!(q.annotations.operation.as_deref(), Some("SECOND"));
656 }
657
658 #[test]
659 fn with_operation_sets_both_fields() {
660 let q = sqlx::query::<Sqlite>("SELECT 1").with_operation("SELECT", "users");
661 assert_eq!(q.annotations.operation.as_deref(), Some("SELECT"));
662 assert_eq!(q.annotations.collection.as_deref(), Some("users"));
663 assert!(q.annotations.query_summary.is_none());
664 assert!(q.annotations.stored_procedure.is_none());
665 }
666
667 #[test]
668 fn with_operation_replaces_previous_annotations() {
669 // Documents the last-call-wins behaviour: with_operation discards earlier fields.
670 let q = sqlx::query::<Sqlite>("SELECT 1")
671 .with_annotations(QueryAnnotations::new().query_summary("legacy summary"))
672 .with_operation("SELECT", "users");
673 assert!(
674 q.annotations.query_summary.is_none(),
675 "with_operation must replace, not merge"
676 );
677 }
678
679 #[test]
680 fn debug_impl_includes_annotations() {
681 let q = sqlx::query::<Sqlite>("SELECT 1")
682 .with_annotations(QueryAnnotations::new().operation("DEBUG_OP"));
683 let debug = format!("{q:?}");
684 assert!(debug.contains("AnnotatedQuery"));
685 assert!(debug.contains("DEBUG_OP"));
686 }
687
688 #[test]
689 fn bind_then_with_annotations_compose() {
690 let q = sqlx::query::<Sqlite>("SELECT ?1, ?2")
691 .bind(1_i32)
692 .with_annotations(QueryAnnotations::new().operation("SELECT"));
693 assert_eq!(q.inner.sql(), "SELECT ?1, ?2");
694 assert_eq!(q.annotations.operation.as_deref(), Some("SELECT"));
695 }
696
697 #[test]
698 fn with_annotations_first_then_bind_compose() {
699 let q = sqlx::query::<Sqlite>("SELECT ?1, ?2")
700 .with_annotations(QueryAnnotations::new().operation("SELECT"))
701 .bind(1_i32);
702 assert_eq!(q.inner.sql(), "SELECT ?1, ?2");
703 assert_eq!(q.annotations.operation.as_deref(), Some("SELECT"));
704 }
705
706 #[test]
707 fn query_as_supports_with_annotations() {
708 let q = sqlx::query_as::<Sqlite, (i32,)>("SELECT 1").with_operation("SELECT", "users");
709 assert_eq!(q.annotations.operation.as_deref(), Some("SELECT"));
710 assert_eq!(q.annotations.collection.as_deref(), Some("users"));
711 }
712
713 #[test]
714 fn query_as_wrapper_with_annotations_replaces() {
715 // Exercise the inherent `with_annotations` on AnnotatedQuery<QueryAs<_>>.
716 let q = sqlx::query_as::<Sqlite, (i32,)>("SELECT 1")
717 .with_annotations(QueryAnnotations::new().operation("FIRST"))
718 .with_annotations(QueryAnnotations::new().operation("SECOND"));
719 assert_eq!(q.annotations.operation.as_deref(), Some("SECOND"));
720 }
721
722 #[test]
723 fn query_as_wrapper_with_operation_chains_on_wrapper() {
724 // First call uses the trait method (on QueryAs); second uses the inherent method
725 // on AnnotatedQuery, which exercises the wrapper-level shorthand.
726 let q = sqlx::query_as::<Sqlite, (i32,)>("SELECT 1")
727 .with_annotations(QueryAnnotations::new().query_summary("legacy"))
728 .with_operation("SELECT", "users");
729 assert_eq!(q.annotations.operation.as_deref(), Some("SELECT"));
730 assert_eq!(q.annotations.collection.as_deref(), Some("users"));
731 assert!(q.annotations.query_summary.is_none());
732 }
733
734 #[test]
735 fn query_as_wrapper_bind_after_annotations() {
736 let q = sqlx::query_as::<Sqlite, (i32,)>("SELECT ?1")
737 .with_annotations(QueryAnnotations::new().operation("SELECT"))
738 .bind(7_i32);
739 assert_eq!(q.inner.sql(), "SELECT ?1");
740 assert_eq!(q.annotations.operation.as_deref(), Some("SELECT"));
741 }
742
743 #[test]
744 fn query_scalar_supports_with_annotations() {
745 let q = sqlx::query_scalar::<Sqlite, i32>("SELECT 1").with_operation("SELECT", "users");
746 assert_eq!(q.annotations.operation.as_deref(), Some("SELECT"));
747 assert_eq!(q.annotations.collection.as_deref(), Some("users"));
748 }
749
750 #[test]
751 fn query_scalar_wrapper_with_annotations_replaces() {
752 let q = sqlx::query_scalar::<Sqlite, i32>("SELECT 1")
753 .with_annotations(QueryAnnotations::new().operation("FIRST"))
754 .with_annotations(QueryAnnotations::new().operation("SECOND"));
755 assert_eq!(q.annotations.operation.as_deref(), Some("SECOND"));
756 }
757
758 #[test]
759 fn query_scalar_wrapper_with_operation_chains_on_wrapper() {
760 let q = sqlx::query_scalar::<Sqlite, i32>("SELECT 1")
761 .with_annotations(QueryAnnotations::new().query_summary("legacy"))
762 .with_operation("SELECT", "users");
763 assert_eq!(q.annotations.operation.as_deref(), Some("SELECT"));
764 assert_eq!(q.annotations.collection.as_deref(), Some("users"));
765 assert!(q.annotations.query_summary.is_none());
766 }
767
768 #[test]
769 fn query_scalar_wrapper_bind_after_annotations() {
770 let q = sqlx::query_scalar::<Sqlite, i32>("SELECT ?1")
771 .with_annotations(QueryAnnotations::new().operation("SELECT"))
772 .bind(7_i32);
773 assert_eq!(q.inner.sql(), "SELECT ?1");
774 assert_eq!(q.annotations.operation.as_deref(), Some("SELECT"));
775 }
776
777 // --- Map / Query::map / Query::try_map composition --------------------
778
779 #[test]
780 fn query_with_annotations_map_preserves_annotations() {
781 // Position 1: annotate before `.map()`. Annotations must survive the wrap.
782 let q = sqlx::query::<Sqlite>("SELECT 1")
783 .with_annotations(QueryAnnotations::new().operation("SELECT"))
784 .map(|_row: sqlx::sqlite::SqliteRow| 42_i64);
785 assert_eq!(q.annotations.operation.as_deref(), Some("SELECT"));
786 }
787
788 #[test]
789 fn query_bind_with_annotations_map_preserves_annotations() {
790 // Position 2: annotate between `.bind()` and `.map()`.
791 let q = sqlx::query::<Sqlite>("SELECT ?1")
792 .bind(1_i32)
793 .with_annotations(QueryAnnotations::new().operation("SELECT"))
794 .map(|_row: sqlx::sqlite::SqliteRow| 42_i64);
795 assert_eq!(q.annotations.operation.as_deref(), Some("SELECT"));
796 }
797
798 #[test]
799 fn query_map_with_annotations_replaces_previous() {
800 // Position 3 + last-call-wins on the new `Map` wrapper.
801 let q = sqlx::query::<Sqlite>("SELECT 1")
802 .map(|_row: sqlx::sqlite::SqliteRow| 42_i64)
803 .with_annotations(QueryAnnotations::new().operation("FIRST"))
804 .with_annotations(QueryAnnotations::new().operation("SECOND"));
805 assert_eq!(q.annotations.operation.as_deref(), Some("SECOND"));
806 }
807
808 #[test]
809 fn query_try_map_with_annotations_compose() {
810 // `try_map` stores `F` directly; sanity-check the non-opaque closure branch.
811 let q = sqlx::query::<Sqlite>("SELECT 1")
812 .try_map(|_row: sqlx::sqlite::SqliteRow| Ok::<_, sqlx::Error>(42_i64))
813 .with_operation("SELECT", "users");
814 assert_eq!(q.annotations.operation.as_deref(), Some("SELECT"));
815 assert_eq!(q.annotations.collection.as_deref(), Some("users"));
816 }
817
818 #[test]
819 fn annotated_query_try_map_preserves_annotations() {
820 // Exercise `AnnotatedQuery<Query>::try_map` (the wrapper's own method, not sqlx's).
821 // Position-1-then-fallible-mapper.
822 let q = sqlx::query::<Sqlite>("SELECT 1")
823 .with_annotations(QueryAnnotations::new().operation("SELECT"))
824 .try_map(|_row: sqlx::sqlite::SqliteRow| Ok::<_, sqlx::Error>(42_i64));
825 assert_eq!(q.annotations.operation.as_deref(), Some("SELECT"));
826 }
827
828 #[test]
829 fn map_compose_after_annotations() {
830 // Multi-map composition: annotations survive across two `.map()` calls.
831 let q = sqlx::query::<Sqlite>("SELECT 1")
832 .with_annotations(QueryAnnotations::new().operation("SELECT"))
833 .map(|_row: sqlx::sqlite::SqliteRow| 1_i64)
834 .map(|n| n + 1);
835 assert_eq!(q.annotations.operation.as_deref(), Some("SELECT"));
836 }
837
838 #[test]
839 fn map_then_with_operation_replaces_via_wrapper() {
840 // `with_operation` shorthand on the new `AnnotatedQuery<Map<...>>` wrapper.
841 let q = sqlx::query::<Sqlite>("SELECT 1")
842 .map(|_row: sqlx::sqlite::SqliteRow| 1_i64)
843 .with_annotations(QueryAnnotations::new().query_summary("legacy"))
844 .with_operation("SELECT", "users");
845 assert_eq!(q.annotations.operation.as_deref(), Some("SELECT"));
846 assert_eq!(q.annotations.collection.as_deref(), Some("users"));
847 assert!(q.annotations.query_summary.is_none());
848 }
849
850 #[test]
851 fn debug_impl_for_annotated_map_includes_annotations() {
852 // The manual `Debug` impl on the new `Map` wrapper still prints annotations.
853 let q = sqlx::query::<Sqlite>("SELECT 1")
854 .map(|_row: sqlx::sqlite::SqliteRow| 1_i64)
855 .with_annotations(QueryAnnotations::new().operation("DEBUG_MAP"));
856 let debug = format!("{q:?}");
857 assert!(debug.contains("AnnotatedQuery"));
858 assert!(debug.contains("DEBUG_MAP"));
859 }
860}