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::{CratestackContext, CratestackError};
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    /// The query's filters, ordering and paging with **no** authorization
78    /// scope: neither this model's read policy nor, inside relation
79    /// filter/sort subqueries, the related model's. Use
80    /// [`Self::preview_scoped_sql`] to see what actually executes.
81    pub fn preview_sql(&self) -> String {
82        super::find_many_preview::preview_sql(self)
83    }
84
85    /// The query as it executes for `ctx`, including the related read
86    /// scope inside every relation filter/sort subquery.
87    pub fn preview_scoped_sql(&self, ctx: &CratestackContext) -> String {
88        super::find_many_preview::preview_scoped_sql(self, ctx)
89    }
90
91    pub async fn run(self, ctx: &CratestackContext) -> Result<Vec<M>, CratestackError>
92    where
93        for<'r> M: Send + Unpin + sqlx::FromRow<'r, sqlx::postgres::PgRow>,
94    {
95        let order_by = self.effective_order_by();
96        let mut query = sqlx::QueryBuilder::<sqlx::Postgres>::new("SELECT ");
97        query
98            .push(self.descriptor.select_projection())
99            .push(" FROM ")
100            .push(self.descriptor.table_name());
101
102        push_scoped_conditions(
103            &mut query,
104            self.descriptor,
105            &self.filters,
106            None::<(&'static str, i64)>,
107            ctx,
108            ReadPolicyKind::List,
109        );
110        push_order_and_paging(&mut query, &order_by, self.limit, self.offset, ctx);
111        if self.for_update {
112            query.push(" FOR UPDATE");
113        }
114
115        query
116            .build_query_as::<M>()
117            .fetch_all(self.runtime.pool())
118            .await
119            .map_err(|error| CratestackError::Database(error.to_string()))
120    }
121
122    /// Run inside a caller-supplied transaction. Required when pairing
123    /// with [`Self::for_update`].
124    pub async fn run_in_tx<'tx>(
125        self,
126        tx: &mut sqlx::Transaction<'tx, sqlx::Postgres>,
127        ctx: &CratestackContext,
128    ) -> Result<Vec<M>, CratestackError>
129    where
130        for<'r> M: Send + Unpin + sqlx::FromRow<'r, sqlx::postgres::PgRow>,
131    {
132        let order_by = self.effective_order_by();
133        let mut query = sqlx::QueryBuilder::<sqlx::Postgres>::new("SELECT ");
134        query
135            .push(self.descriptor.select_projection())
136            .push(" FROM ")
137            .push(self.descriptor.table_name());
138
139        push_scoped_conditions(
140            &mut query,
141            self.descriptor,
142            &self.filters,
143            None::<(&'static str, i64)>,
144            ctx,
145            ReadPolicyKind::List,
146        );
147        push_order_and_paging(&mut query, &order_by, self.limit, self.offset, ctx);
148        if self.for_update {
149            query.push(" FOR UPDATE");
150        }
151
152        query
153            .build_query_as::<M>()
154            .fetch_all(&mut **tx)
155            .await
156            .map_err(|error| CratestackError::Database(error.to_string()))
157    }
158
159    pub(super) fn effective_order_by(&self) -> Vec<OrderClause> {
160        let mut order_by = self.order_by.clone();
161        let Some(direction) = order_by
162            .iter()
163            .find(|clause| clause.is_relation_scalar())
164            .map(OrderClause::direction)
165        else {
166            return order_by;
167        };
168
169        if order_by
170            .iter()
171            .any(|clause| clause.targets_column(self.descriptor.primary_key()))
172        {
173            return order_by;
174        }
175
176        order_by.push(OrderClause::column(
177            self.descriptor.primary_key(),
178            direction,
179        ));
180        order_by
181    }
182
183    /// Side-load a to-one relation alongside the matched rows. Two
184    /// queries, not a SQL JOIN, so the related-side read policy +
185    /// soft-delete inherit from `find_many` for free.
186    pub fn include<Rel, RelPK>(
187        self,
188        relation: cratestack_sql::RelationInclude<M, Rel, RelPK>,
189    ) -> super::find_many_with::FindManyWith<'a, M, PK, Rel, RelPK>
190    where
191        Rel: 'static,
192        RelPK: 'static,
193    {
194        super::find_many_with::FindManyWith::new(self, relation)
195    }
196}