Skip to main content

cratestack_sqlx/query/read/
find_many.rs

1//! `find_many` — typed multi-row read with filter / order /
2//! pagination / `FOR UPDATE`. The `preview_*_sql` previews live in
3//! [`super::find_many_preview`] to keep this file under budget.
4
5use cratestack_core::{CoolContext, CoolError};
6use cratestack_sql::ReadSource;
7
8use crate::query::support::{ReadPolicyKind, push_order_and_paging, push_scoped_conditions};
9use crate::{FilterExpr, OrderClause, SqlxRuntime, sqlx};
10
11#[derive(Clone)]
12pub struct FindMany<'a, M: 'static, PK: 'static> {
13    pub(crate) runtime: &'a SqlxRuntime,
14    /// Either a `&'static ModelDescriptor<M, PK>` (typical) or a
15    /// `&'static ViewDescriptor<M, PK>` (view path). Both impl
16    /// `ReadSource<M, PK>`.
17    pub(crate) descriptor: &'static dyn ReadSource<M, PK>,
18    pub(crate) filters: Vec<FilterExpr>,
19    pub(crate) order_by: Vec<OrderClause>,
20    pub(crate) limit: Option<i64>,
21    pub(crate) offset: Option<i64>,
22    pub(crate) for_update: bool,
23}
24
25impl<'a, M: 'static, PK: 'static> FindMany<'a, M, PK> {
26    pub fn where_(mut self, filter: crate::Filter) -> Self {
27        self.filters.push(FilterExpr::from(filter));
28        self
29    }
30
31    pub fn where_expr(mut self, filter: FilterExpr) -> Self {
32        self.filters.push(filter);
33        self
34    }
35
36    pub fn where_any(mut self, filters: impl IntoIterator<Item = FilterExpr>) -> Self {
37        self.filters.push(FilterExpr::any(filters));
38        self
39    }
40
41    /// Conditionally append a filter. `None` is a no-op so callers can
42    /// pipe `FieldRef::match_optional(...)` results straight in
43    /// without an `if let` ladder at every optional-param site.
44    pub fn where_optional<F>(mut self, filter: Option<F>) -> Self
45    where
46        F: Into<FilterExpr>,
47    {
48        if let Some(filter) = filter {
49            self.filters.push(filter.into());
50        }
51        self
52    }
53
54    pub fn order_by(mut self, clause: OrderClause) -> Self {
55        self.order_by.push(clause);
56        self
57    }
58
59    pub fn limit(mut self, limit: i64) -> Self {
60        self.limit = Some(limit);
61        self
62    }
63
64    pub fn offset(mut self, offset: i64) -> Self {
65        self.offset = Some(offset);
66        self
67    }
68
69    /// Emit `SELECT ... FOR UPDATE` so the engine takes an exclusive
70    /// row-level lock on every matched row for the surrounding
71    /// transaction. Only meaningful when paired with [`Self::run_in_tx`].
72    pub fn for_update(mut self) -> Self {
73        self.for_update = true;
74        self
75    }
76
77    pub fn preview_sql(&self) -> String {
78        super::find_many_preview::preview_sql(self)
79    }
80
81    pub fn preview_scoped_sql(&self, ctx: &CoolContext) -> String {
82        super::find_many_preview::preview_scoped_sql(self, ctx)
83    }
84
85    pub async fn run(self, ctx: &CoolContext) -> Result<Vec<M>, CoolError>
86    where
87        for<'r> M: Send + Unpin + sqlx::FromRow<'r, sqlx::postgres::PgRow>,
88    {
89        let order_by = self.effective_order_by();
90        let mut query = sqlx::QueryBuilder::<sqlx::Postgres>::new("SELECT ");
91        query
92            .push(self.descriptor.select_projection())
93            .push(" FROM ")
94            .push(self.descriptor.table_name());
95
96        push_scoped_conditions(
97            &mut query,
98            self.descriptor,
99            &self.filters,
100            None::<(&'static str, i64)>,
101            ctx,
102            ReadPolicyKind::List,
103        );
104        push_order_and_paging(&mut query, &order_by, self.limit, self.offset);
105        if self.for_update {
106            query.push(" FOR UPDATE");
107        }
108
109        query
110            .build_query_as::<M>()
111            .fetch_all(self.runtime.pool())
112            .await
113            .map_err(|error| CoolError::Database(error.to_string()))
114    }
115
116    /// Run inside a caller-supplied transaction. Required when pairing
117    /// with [`Self::for_update`].
118    pub async fn run_in_tx<'tx>(
119        self,
120        tx: &mut sqlx::Transaction<'tx, sqlx::Postgres>,
121        ctx: &CoolContext,
122    ) -> Result<Vec<M>, CoolError>
123    where
124        for<'r> M: Send + Unpin + sqlx::FromRow<'r, sqlx::postgres::PgRow>,
125    {
126        let order_by = self.effective_order_by();
127        let mut query = sqlx::QueryBuilder::<sqlx::Postgres>::new("SELECT ");
128        query
129            .push(self.descriptor.select_projection())
130            .push(" FROM ")
131            .push(self.descriptor.table_name());
132
133        push_scoped_conditions(
134            &mut query,
135            self.descriptor,
136            &self.filters,
137            None::<(&'static str, i64)>,
138            ctx,
139            ReadPolicyKind::List,
140        );
141        push_order_and_paging(&mut query, &order_by, self.limit, self.offset);
142        if self.for_update {
143            query.push(" FOR UPDATE");
144        }
145
146        query
147            .build_query_as::<M>()
148            .fetch_all(&mut **tx)
149            .await
150            .map_err(|error| CoolError::Database(error.to_string()))
151    }
152
153    pub(super) fn effective_order_by(&self) -> Vec<OrderClause> {
154        let mut order_by = self.order_by.clone();
155        let Some(direction) = order_by
156            .iter()
157            .find(|clause| clause.is_relation_scalar())
158            .map(OrderClause::direction)
159        else {
160            return order_by;
161        };
162
163        if order_by
164            .iter()
165            .any(|clause| clause.targets_column(self.descriptor.primary_key()))
166        {
167            return order_by;
168        }
169
170        order_by.push(OrderClause::column(self.descriptor.primary_key(), direction));
171        order_by
172    }
173
174    /// Side-load a to-one relation alongside the matched rows. Two
175    /// queries, not a SQL JOIN, so the related-side read policy +
176    /// soft-delete inherit from `find_many` for free.
177    pub fn include<Rel, RelPK>(
178        self,
179        relation: cratestack_sql::RelationInclude<M, Rel, RelPK>,
180    ) -> super::find_many_with::FindManyWith<'a, M, PK, Rel, RelPK>
181    where
182        Rel: 'static,
183        RelPK: 'static,
184    {
185        super::find_many_with::FindManyWith::new(self, relation)
186    }
187}