Skip to main content

es_entity/
query.rs

1//! Query execution infrastructure for event-sourced entities.
2//!
3//! This module provides the underlying query types used by the `es_query!` macro.
4//! **These types are not intended to be used directly** - instead, use the `es_query!`
5//! macro which provides a simpler interface for querying index tables and automatically
6//! hydrating entities from their events.
7//!
8//! # Example
9//!
10//! Instead of using these types directly, use the `es_query!` macro:
11//!
12//! ```rust,ignore
13//! es_query!(
14//!     "SELECT id FROM users WHERE name = $1",
15//!     name
16//! ).fetch_optional(&pool).await
17//! ```
18//!
19//! See the `es_query!` macro documentation for more details.
20
21use crate::{
22    db,
23    error::EntityHydrationError,
24    events::{EntityEvents, HydrationRow},
25    one_time_executor::IntoOneTimeExecutor,
26    snapshot::NO_SNAPSHOT_FINGERPRINT,
27    traits::*,
28    tree_query::{TreeQuerySource, build_tree_query, partition_by_tag, snapshot_fingerprints},
29};
30
31/// Query builder for event-sourced entities.
32///
33/// This type is generated by the `es_query!` macro and should not be constructed directly.
34/// It wraps a SQLx query and provides methods to fetch and hydrate entities from their events.
35///
36/// `R` is the row type the underlying `sqlx::query_as!` decodes into —
37/// `GenericEvent<Id>` for a plain repo, `SnapshotGenericEvent<Id>` for a
38/// `#[es_repo(snapshot)]` repo — normalised into a `HydrationRow<Id>` before
39/// an entity is built from it.
40pub struct EsQuery<
41    'q,
42    Repo,
43    Flavor,
44    F,
45    A,
46    R = crate::events::GenericEvent<
47        <<<Repo as EsRepo>::Entity as EsEntity>::Event as EsEvent>::EntityId,
48    >,
49> where
50    Repo: EsRepo,
51{
52    inner: sqlx::query::Map<'q, db::Db, F, A>,
53    source: TreeQuerySource<<<<Repo as EsRepo>::Entity as EsEntity>::Event as EsEvent>::EntityId>,
54    _repo: std::marker::PhantomData<Repo>,
55    _flavor: std::marker::PhantomData<Flavor>,
56    _row: std::marker::PhantomData<fn() -> R>,
57}
58
59/// Query flavor for flat entities without nested relationships.
60pub struct EsQueryFlavorFlat;
61
62/// Query flavor for entities with nested relationships that need to be loaded recursively.
63pub struct EsQueryFlavorNested;
64
65impl<'q, Repo, Flavor, F, A, R> EsQuery<'q, Repo, Flavor, F, A, R>
66where
67    Repo: EsRepo,
68    <<<Repo as EsRepo>::Entity as EsEntity>::Event as EsEvent>::EntityId: Unpin,
69    F: FnMut(db::Row) -> Result<R, sqlx::Error> + Send,
70    R: Into<HydrationRow<<<<Repo as EsRepo>::Entity as EsEntity>::Event as EsEvent>::EntityId>>
71        + Send
72        + Unpin,
73    A: 'q + Send + sqlx::IntoArguments<'q, db::Db>,
74{
75    pub fn new(
76        query: sqlx::query::Map<'q, db::Db, F, A>,
77        source: TreeQuerySource<
78            <<<Repo as EsRepo>::Entity as EsEntity>::Event as EsEvent>::EntityId,
79        >,
80    ) -> Self {
81        Self {
82            inner: query,
83            source,
84            _repo: std::marker::PhantomData,
85            _flavor: std::marker::PhantomData,
86            _row: std::marker::PhantomData,
87        }
88    }
89
90    async fn fetch_optional_inner<E: From<sqlx::Error> + From<EntityHydrationError>>(
91        self,
92        op: impl IntoOneTimeExecutor<'_>,
93    ) -> Result<Option<<Repo as EsRepo>::Entity>, E> {
94        let executor = op.into_executor();
95        let rows = executor.fetch_all(self.inner).await?;
96        if rows.is_empty() {
97            return Ok(None);
98        }
99
100        Ok(EntityEvents::load_first(rows.into_iter())?)
101    }
102
103    async fn fetch_n_inner<E: From<sqlx::Error> + From<EntityHydrationError>>(
104        self,
105        op: impl IntoOneTimeExecutor<'_>,
106        first: usize,
107    ) -> Result<(Vec<<Repo as EsRepo>::Entity>, bool), E> {
108        let executor = op.into_executor();
109        let rows = executor.fetch_all(self.inner).await?;
110        Ok(EntityEvents::load_n(rows.into_iter(), first)?)
111    }
112
113    async fn fetch_tree_rows<E: From<sqlx::Error>>(
114        self,
115        op: impl IntoOneTimeExecutor<'_>,
116        include_deleted: bool,
117    ) -> Result<
118        (
119            Vec<HydrationRow<<<<Repo as EsRepo>::Entity as EsEntity>::Event as EsEvent>::EntityId>>,
120            std::collections::HashMap<i32, Vec<db::Row>>,
121        ),
122        E,
123    > {
124        let executor = op.into_executor();
125        let spec = <Repo as EsRepo>::nested_tree_spec();
126        let sql = build_tree_query(
127            self.source.user_sql,
128            self.source.order_by_cols,
129            &spec,
130            include_deleted,
131            self.source.n_user_args + 1,
132        );
133
134        let mut inner = self.inner;
135        let mut args = sqlx::Execute::take_arguments(&mut inner)
136            .map_err(sqlx::Error::Encode)?
137            .unwrap_or_default();
138
139        // A full-history load binds `NO_SNAPSHOT_FINGERPRINT` as the root's
140        // own fingerprint, forcing every node in the tree to bind the
141        // sentinel too. The `snapshot_table_name` check guards against a
142        // false positive: `NoSnapshot::FINGERPRINT` *equals* that sentinel,
143        // so an ordinary non-snapshot root would otherwise always match.
144        let full_history = spec.snapshot_table_name.is_some()
145            && self.source.snapshot_fingerprint == NO_SNAPSHOT_FINGERPRINT;
146        // The root's own `es_query!` call site already bound its
147        // fingerprint, so only descendants need binding here — walked in
148        // the same DFS order `build_tree_query` assigned positions in.
149        let mut fingerprints = snapshot_fingerprints(&spec).into_iter();
150        if spec.snapshot_table_name.is_some() {
151            fingerprints.next();
152        }
153        for fp in fingerprints {
154            let bind = if full_history {
155                NO_SNAPSHOT_FINGERPRINT
156            } else {
157                fp
158            };
159            sqlx::Arguments::add(&mut args, bind).map_err(sqlx::Error::Encode)?;
160        }
161
162        let rows: Vec<db::Row> = sqlx::query_with::<db::Db, _>(&sql, args)
163            .fetch_all(executor)
164            .await?;
165        let mut by_tag = partition_by_tag(rows)?;
166        let root_rows = by_tag.remove(&0).unwrap_or_default();
167        let root = root_rows
168            .iter()
169            .map(self.source.decode)
170            .collect::<Result<Vec<_>, sqlx::Error>>()?;
171        Ok((root, by_tag))
172    }
173}
174
175impl<'q, Repo, F, A, R> EsQuery<'q, Repo, EsQueryFlavorFlat, F, A, R>
176where
177    Repo: EsRepo,
178    <<<Repo as EsRepo>::Entity as EsEntity>::Event as EsEvent>::EntityId: Unpin,
179    F: FnMut(db::Row) -> Result<R, sqlx::Error> + Send,
180    R: Into<HydrationRow<<<<Repo as EsRepo>::Entity as EsEntity>::Event as EsEvent>::EntityId>>
181        + Send
182        + Unpin,
183    A: 'q + Send + sqlx::IntoArguments<'q, db::Db>,
184{
185    /// Fetches at most one entity from the query results.
186    ///
187    /// Returns `Ok(None)` if no entities match the query, or `Ok(Some(entity))` if found.
188    pub async fn fetch_optional(
189        self,
190        op: impl IntoOneTimeExecutor<'_>,
191    ) -> Result<Option<<Repo as EsRepo>::Entity>, <Repo as EsRepo>::QueryError> {
192        self.fetch_optional_inner(op).await
193    }
194
195    /// Fetches up to `first` entities from the query results.
196    ///
197    /// Returns a tuple of (entities, has_more) where `has_more` indicates if there
198    /// were more entities available beyond the requested limit.
199    pub async fn fetch_n(
200        self,
201        op: impl IntoOneTimeExecutor<'_>,
202        first: usize,
203    ) -> Result<(Vec<<Repo as EsRepo>::Entity>, bool), <Repo as EsRepo>::QueryError> {
204        self.fetch_n_inner(op, first).await
205    }
206}
207
208impl<'q, Repo, F, A, R> EsQuery<'q, Repo, EsQueryFlavorNested, F, A, R>
209where
210    Repo: EsRepo,
211    <<<Repo as EsRepo>::Entity as EsEntity>::Event as EsEvent>::EntityId: Unpin,
212    F: FnMut(db::Row) -> Result<R, sqlx::Error> + Send,
213    R: Into<HydrationRow<<<<Repo as EsRepo>::Entity as EsEntity>::Event as EsEvent>::EntityId>>
214        + Send
215        + Unpin,
216    A: 'q + Send + sqlx::IntoArguments<'q, db::Db>,
217{
218    /// Fetches at most one entity and loads all nested relationships.
219    ///
220    /// Returns `Ok(None)` if no entities match, or `Ok(Some(entity))` with all
221    /// nested entities loaded if found.
222    pub async fn fetch_optional(
223        self,
224        op: impl IntoOneTimeExecutor<'_>,
225    ) -> Result<Option<<Repo as EsRepo>::Entity>, <Repo as EsRepo>::QueryError> {
226        self.fetch_optional_tree(op, false).await
227    }
228
229    /// Fetches up to `first` entities and loads all nested relationships.
230    ///
231    /// Returns a tuple of (entities, has_more) where all entities have their nested
232    /// relationships loaded, and `has_more` indicates if more entities were available.
233    pub async fn fetch_n(
234        self,
235        op: impl IntoOneTimeExecutor<'_>,
236        first: usize,
237    ) -> Result<(Vec<<Repo as EsRepo>::Entity>, bool), <Repo as EsRepo>::QueryError> {
238        self.fetch_n_tree(op, first, false).await
239    }
240
241    /// Like [`fetch_optional`](EsQuery::fetch_optional) but transitively includes
242    /// soft-deleted nested entities.
243    pub async fn fetch_optional_include_deleted(
244        self,
245        op: impl IntoOneTimeExecutor<'_>,
246    ) -> Result<Option<<Repo as EsRepo>::Entity>, <Repo as EsRepo>::QueryError> {
247        self.fetch_optional_tree(op, true).await
248    }
249
250    /// Like [`fetch_n`](EsQuery::fetch_n) but transitively includes soft-deleted
251    /// nested entities.
252    pub async fn fetch_n_include_deleted(
253        self,
254        op: impl IntoOneTimeExecutor<'_>,
255        first: usize,
256    ) -> Result<(Vec<<Repo as EsRepo>::Entity>, bool), <Repo as EsRepo>::QueryError> {
257        self.fetch_n_tree(op, first, true).await
258    }
259
260    async fn fetch_optional_tree(
261        self,
262        op: impl IntoOneTimeExecutor<'_>,
263        include_deleted: bool,
264    ) -> Result<Option<<Repo as EsRepo>::Entity>, <Repo as EsRepo>::QueryError> {
265        let (root, mut by_tag) = self
266            .fetch_tree_rows::<<Repo as EsRepo>::QueryError>(op, include_deleted)
267            .await?;
268        let Some(entity) = EntityEvents::load_first::<<Repo as EsRepo>::Entity>(root)? else {
269            return Ok(None);
270        };
271        let mut entities = [entity];
272        let mut cursor = 1i32;
273        <Repo as EsRepo>::hydrate_nested_from_rows::<<Repo as EsRepo>::QueryError>(
274            &mut by_tag,
275            &mut cursor,
276            &mut entities,
277        )?;
278        let [entity] = entities;
279        Ok(Some(entity))
280    }
281
282    async fn fetch_n_tree(
283        self,
284        op: impl IntoOneTimeExecutor<'_>,
285        first: usize,
286        include_deleted: bool,
287    ) -> Result<(Vec<<Repo as EsRepo>::Entity>, bool), <Repo as EsRepo>::QueryError> {
288        let (root, mut by_tag) = self
289            .fetch_tree_rows::<<Repo as EsRepo>::QueryError>(op, include_deleted)
290            .await?;
291        let (mut entities, more) = EntityEvents::load_n::<<Repo as EsRepo>::Entity>(root, first)?;
292        let mut cursor = 1i32;
293        <Repo as EsRepo>::hydrate_nested_from_rows::<<Repo as EsRepo>::QueryError>(
294            &mut by_tag,
295            &mut cursor,
296            &mut entities,
297        )?;
298        Ok((entities, more))
299    }
300}