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