Skip to main content

cratestack_sqlx/query/read/
aggregate_count.rs

1//! `aggregate.count()` — `COUNT(*)` with filter + read policy.
2
3use cratestack_core::{CratestackContext, CratestackError};
4use cratestack_sql::ReadSource;
5
6use crate::query::support::{ReadPolicyKind, push_scoped_conditions};
7use crate::{FilterExpr, SqlxRuntime, sqlx};
8
9use super::find_many::FindMany;
10
11#[derive(Clone)]
12pub struct AggregateCount<'a, M: 'static, PK: 'static> {
13    runtime: &'a SqlxRuntime,
14    descriptor: &'static dyn ReadSource<M, PK>,
15    filters: Vec<FilterExpr>,
16}
17
18/// Reuses the exact `filters` a `find_many` builder assembled for a
19/// `FindMany`, discarding `order_by`/`limit`/`offset`/`for_update` —
20/// meaningless for a scalar `COUNT(*)`. Both `FindMany::run` and
21/// `AggregateCount::run` push their WHERE clause through the same
22/// [`push_scoped_conditions`] with the same descriptor and
23/// [`ReadPolicyKind::List`], so transferring `filters` verbatim (rather
24/// than re-deriving them from the caller's query a second time) is what
25/// guarantees the count can't apply a different `WHERE` clause or
26/// policy scope than the page it describes — see cratestack#570, whose
27/// whole risk was exactly that kind of divergence.
28impl<'a, M: 'static, PK: 'static> From<FindMany<'a, M, PK>> for AggregateCount<'a, M, PK> {
29    fn from(find_many: FindMany<'a, M, PK>) -> Self {
30        Self {
31            runtime: find_many.runtime,
32            descriptor: find_many.descriptor,
33            filters: find_many.filters,
34        }
35    }
36}
37
38impl<'a, M: 'static, PK: 'static> AggregateCount<'a, M, PK> {
39    pub(super) fn new(
40        runtime: &'a SqlxRuntime,
41        descriptor: &'static dyn ReadSource<M, PK>,
42    ) -> Self {
43        Self {
44            runtime,
45            descriptor,
46            filters: Vec::new(),
47        }
48    }
49
50    pub fn where_(mut self, filter: crate::Filter) -> Self {
51        self.filters.push(FilterExpr::from(filter));
52        self
53    }
54
55    pub fn where_expr(mut self, filter: FilterExpr) -> Self {
56        self.filters.push(filter);
57        self
58    }
59
60    pub fn where_any(mut self, filters: impl IntoIterator<Item = FilterExpr>) -> Self {
61        self.filters.push(FilterExpr::any(filters));
62        self
63    }
64
65    pub fn where_optional<F>(mut self, filter: Option<F>) -> Self
66    where
67        F: Into<FilterExpr>,
68    {
69        if let Some(filter) = filter {
70            self.filters.push(filter.into());
71        }
72        self
73    }
74
75    fn build_query<'q>(&self, ctx: &CratestackContext) -> sqlx::QueryBuilder<'q, sqlx::Postgres> {
76        let mut query = sqlx::QueryBuilder::<sqlx::Postgres>::new("SELECT COUNT(*) FROM ");
77        query.push(self.descriptor.table_name());
78        push_scoped_conditions(
79            &mut query,
80            self.descriptor,
81            &self.filters,
82            None::<(&'static str, i64)>,
83            ctx,
84            ReadPolicyKind::List,
85        );
86        query
87    }
88
89    /// The exact `COUNT(*)` SQL this would run, without executing it —
90    /// built by the same `build_query` that `run`/`run_in_tx` use, so
91    /// this can't drift from what actually gets sent (unlike a
92    /// hand-rolled preview string-builder). No live DB connection is
93    /// required: `QueryBuilder` assembly is pure string/bind-slot
94    /// bookkeeping.
95    pub fn preview_scoped_sql(&self, ctx: &CratestackContext) -> String {
96        self.build_query(ctx).sql().to_owned()
97    }
98
99    pub async fn run(self, ctx: &CratestackContext) -> Result<i64, CratestackError> {
100        let mut query = self.build_query(ctx);
101        let value: (i64,) = query
102            .build_query_as::<(i64,)>()
103            .fetch_one(self.runtime.pool())
104            .await
105            .map_err(|error| CratestackError::Database(error.to_string()))?;
106        Ok(value.0)
107    }
108
109    pub async fn run_in_tx<'tx>(
110        self,
111        tx: &mut sqlx::Transaction<'tx, sqlx::Postgres>,
112        ctx: &CratestackContext,
113    ) -> Result<i64, CratestackError> {
114        let mut query = self.build_query(ctx);
115        let value: (i64,) = query
116            .build_query_as::<(i64,)>()
117            .fetch_one(&mut **tx)
118            .await
119            .map_err(|error| CratestackError::Database(error.to_string()))?;
120        Ok(value.0)
121    }
122}