Skip to main content

post_archiver/query/
mod.rs

1//! Query builder module providing fluent read-only operations for all entities.
2//!
3//! # Architecture
4//!
5//! Each query builder (e.g. [`PostQuery`](post::PostQuery)) exposes a set of public filter
6//! fields (from the [`filter`] module: `IdFilter`, `TextFilter`, `DateFilter`,
7//! `RelationshipsFilter`). Callers mutate those fields directly, then call
8//! [`Query::query()`] to execute the SQL and retrieve results.
9//!
10//! Builders can be composed via decorator wrappers:
11//! - [`Paginate::pagination(limit, page)`](Paginate::pagination) → [`Paginated<Q>`](Paginated): appends `LIMIT … OFFSET …`
12//! - [`Sortable::sort(field, dir)`](Sortable::sort) → [`Sorted<Q, F>`](sortable::Sorted): appends `ORDER BY …`
13//! - [`Sortable::sort_random()`](Sortable::sort_random): appends `ORDER BY RANDOM()`
14//! - [`Countable::with_total()`](Countable::with_total) → [`WithTotal<Q>`](countable::WithTotal): also returns the total row count
15//!
16//! # Entry points on [`PostArchiverManager`](crate::manager::PostArchiverManager)
17//!
18//! | Method | Builder | Returns |
19//! |--------|---------|----------|
20//! | `manager.posts()` | [`PostQuery`](post::PostQuery) | `Vec<Post>` / `Totalled<Vec<Post>>` |
21//! | `manager.authors()` | [`AuthorQuery`](author::AuthorQuery) | `Vec<Author>` / `Totalled<Vec<Author>>` |
22//! | `manager.tags()` | [`TagQuery`](tag::TagQuery) | `Vec<Tag>` / `Totalled<Vec<Tag>>` |
23//! | `manager.platforms()` | [`PlatformQuery`](platform::PlatformQuery) | `Vec<Platform>` |
24//! | `manager.collections()` | [`CollectionQuery`](collection::CollectionQuery) | `Vec<Collection>` / `Totalled<Vec<Collection>>` |
25//!
26//! # Examples
27//!
28//! ```no_run
29//! # use post_archiver::manager::PostArchiverManager;
30//! # use post_archiver::query::{Countable, Paginate, Query};
31//! # let manager = PostArchiverManager::open_in_memory().unwrap();
32//! // All matching posts (no LIMIT)
33//! let v = manager.posts().query::<post_archiver::Post>().unwrap();
34//!
35//! // Paginated — page 0, 20 items per page
36//! let v = manager.posts().pagination(20, 0).query::<post_archiver::Post>().unwrap();
37//!
38//! // Paginated + total count
39//! let p = manager.posts().pagination(20, 0).with_total().query::<post_archiver::Post>().unwrap();
40//! println!("{} total posts", p.total);
41//! ```
42
43pub mod author;
44pub mod collection;
45pub mod file_meta;
46pub mod filter;
47pub mod platform;
48pub mod post;
49pub mod tag;
50
51use cached::Cached;
52pub use countable::{Countable, Totalled};
53pub use paginate::Paginate;
54pub use sortable::{SortDir, Sortable};
55
56use std::{
57    fmt::{Debug, Display},
58    rc::Rc,
59};
60
61use rusqlite::ToSql;
62
63use crate::{
64    manager::{PostArchiverConnection, PostArchiverManager},
65    utils::macros::AsTable,
66};
67
68/// Trait for types that can be deserialized from a SQL row.
69///
70/// Implement this trait to make a type queryable as a generic parameter of [`Query::query()`].
71/// Typically implemented via `#[derive]` macros or manually on entity structs
72/// (e.g. [`Post`](crate::Post), [`Author`](crate::Author)).
73///
74/// # Associated types
75/// - `Based`: the corresponding database table type, must implement [`AsTable`].
76pub trait FromQuery: Sized {
77    type Based: AsTable;
78    /// Returns the `SELECT …` SQL fragment used to query this type (without WHERE/ORDER/LIMIT).
79    fn select_sql() -> String;
80    /// Deserializes one instance of this type from a rusqlite `Row`.
81    fn from_row(row: &rusqlite::Row) -> Result<Self, rusqlite::Error>;
82}
83
84// ── Query Trait ───────────────────────────────────────────────────────────────
85
86/// Core query execution trait. Implementors can call `.query()` to run SQL and retrieve results.
87///
88/// You generally do not implement this trait directly; use the concrete query builders
89/// (e.g. [`PostQuery`](post::PostQuery), [`AuthorQuery`](author::AuthorQuery)).
90///
91/// The decorator wrappers [`Paginated`], [`WithTotal`](countable::WithTotal), and
92/// [`Sorted`](sortable::Sorted) all implement this trait by wrapping and delegating.
93pub trait Query: Sized {
94    /// The wrapper type for query results. For most builders this is `Vec<T>`;
95    /// when wrapped by [`WithTotal`](countable::WithTotal) it becomes [`Totalled<Vec<T>>`](Totalled).
96    type Wrapper<T>;
97    /// The database table type this query targets.
98    type Based: AsTable;
99    /// Execute the query with an externally supplied [`RawSql`] context
100    /// (typically threaded through decorator layers).
101    fn query_with_context<T: FromQuery<Based = Self::Based>>(
102        self,
103        sql: RawSql<T>,
104    ) -> crate::error::Result<Self::Wrapper<T>>;
105
106    /// Execute the query with a default empty [`RawSql`] context, returning all matching results.
107    fn query<T: FromQuery<Based = Self::Based>>(self) -> crate::error::Result<Self::Wrapper<T>> {
108        let sql: RawSql<T> = RawSql::new();
109        self.query_with_context(sql)
110    }
111}
112
113/// Trait for types that hold filter conditions and can write them into a [`RawSql`].
114///
115/// Query builders (e.g. [`PostQuery`](post::PostQuery)) and decorator wrappers (e.g. [`Paginated`])
116/// implement this trait so that [`BaseFilter::count()`] can compute the total matching row count.
117/// The default `count()` implementation caches its result to avoid redundant queries.
118pub trait BaseFilter: Sized {
119    type Based: AsTable;
120    /// Append all filter conditions held by this builder into `sql` and return the updated [`RawSql`].
121    fn update_sql<T: FromQuery<Based = Self::Based>>(&self, sql: RawSql<T>) -> RawSql<T>;
122    /// Return a reference to the [`Queryer`] owned by this builder, used by the default `count()` impl.
123    fn queryer(&self) -> &Queryer<'_, impl PostArchiverConnection>;
124
125    fn count(&self) -> crate::error::Result<u64> {
126        let sql = RawSql::<Self::Based>::new();
127        let sql = self.update_sql(sql);
128        let (sql, params) = sql.build_count_sql();
129
130        let cache_key = (
131            sql.clone(),
132            params
133                .iter()
134                .map(|p| p.to_string())
135                .collect::<Vec<_>>()
136                .join(","),
137        );
138
139        let cached_count = self
140            .queryer()
141            .manager
142            .caches
143            .lock()
144            .unwrap()
145            .counts
146            .cache_get(&cache_key)
147            .cloned();
148        match cached_count {
149            Some(cached_count) => Ok(cached_count),
150            None => {
151                let count = self.queryer().count(&sql, params)?;
152                self.queryer()
153                    .manager
154                    .caches
155                    .lock()
156                    .unwrap()
157                    .counts
158                    .cache_set(cache_key, count);
159                Ok(count)
160            }
161        }
162    }
163}
164
165pub trait ToSqlAndEq: ToSql + Display {}
166impl<T: ToSql + Display> ToSqlAndEq for T {}
167
168pub type Param = Rc<dyn ToSqlAndEq>;
169
170/// Intermediate SQL representation passed through the decorator chain and filled layer by layer.
171///
172/// Received by [`Query::query_with_context()`]; filters append WHERE conditions via
173/// [`BaseFilter::update_sql()`]; [`Paginated`] sets `limit_clause`;
174/// [`Sorted`](sortable::Sorted) appends to `order_clause`. Finally assembled into a
175/// complete SQL string by [`RawSql::build_sql()`].
176#[derive(Default, Clone)]
177pub struct RawSql<T> {
178    /// WHERE clause: `(list of condition strings, list of bound parameters)`, joined with `AND`.
179    pub where_clause: (Vec<String>, Vec<Param>),
180    /// ORDER BY expressions accumulated in append order.
181    pub order_clause: Vec<String>,
182    /// `[limit, offset]` corresponding to `LIMIT ? OFFSET ?`. `None` means no pagination.
183    pub limit_clause: Option<[u64; 2]>,
184    _phantom: std::marker::PhantomData<T>,
185}
186
187impl<T> Debug for RawSql<T> {
188    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
189        f.debug_struct("RawSql")
190            .field("where_clause", &self.where_clause.0)
191            .field("order_clause", &self.order_clause)
192            .field("limit_clause", &self.limit_clause)
193            .finish()
194    }
195}
196
197impl<T> RawSql<T> {
198    /// Create an empty [`RawSql`] with all fields at their defaults (no WHERE / ORDER / LIMIT).
199    pub fn new() -> Self {
200        Self {
201            where_clause: (Vec::new(), Vec::new()),
202            order_clause: Vec::new(),
203            limit_clause: None,
204            _phantom: std::marker::PhantomData,
205        }
206    }
207
208    /// Build the `WHERE … ORDER BY … LIMIT ? OFFSET ?` fragment (without the SELECT prefix).
209    pub fn build_generic_sql(&self) -> (String, Vec<Param>) {
210        let mut params = self.where_clause.1.clone();
211        let where_sql = if self.where_clause.0.is_empty() {
212            "".to_string()
213        } else {
214            format!("WHERE {}", self.where_clause.0.join(" AND "))
215        };
216        let order_sql = if self.order_clause.is_empty() {
217            "".to_string()
218        } else {
219            format!("ORDER BY {}", self.order_clause.join(", "))
220        };
221        let limit_sql = if let Some([limit, offset]) = self.limit_clause {
222            params.push(Rc::new(limit));
223            params.push(Rc::new(offset));
224            "LIMIT ? OFFSET ?".to_string()
225        } else {
226            "".to_string()
227        };
228        (format!("{} {} {}", where_sql, order_sql, limit_sql), params)
229    }
230}
231
232impl<T: FromQuery> RawSql<T> {
233    /// Build the full `SELECT … WHERE … ORDER BY … LIMIT ? OFFSET ?` SQL string.
234    pub fn build_sql(&self) -> (String, Vec<Param>) {
235        let (clause, params) = self.build_generic_sql();
236        let sql = format!("{} {}", T::select_sql(), clause);
237        (sql, params)
238    }
239
240    /// Build a `SELECT COUNT(*) FROM <table> WHERE …` SQL string (ORDER/LIMIT are ignored).
241    pub fn build_count_sql(&self) -> (String, Vec<Param>) {
242        let where_sql = if self.where_clause.0.is_empty() {
243            "".to_string()
244        } else {
245            format!("WHERE {}", self.where_clause.0.join(" AND "))
246        };
247        let sql = format!(
248            "SELECT COUNT(*) FROM {} {}",
249            T::Based::TABLE_NAME,
250            where_sql
251        );
252        (sql, self.where_clause.1.clone())
253    }
254}
255
256/// Helper that wraps a [`PostArchiverManager`] reference and provides low-level SQL execution.
257///
258/// Each query builder owns a `Queryer` at construction time and uses it to
259/// access the database connection and result cache.
260pub struct Queryer<'a, C> {
261    pub manager: &'a PostArchiverManager<C>,
262}
263
264impl<T> Debug for Queryer<'_, T> {
265    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
266        f.debug_struct("Queryer").finish()
267    }
268}
269
270impl<'a, C: PostArchiverConnection> Queryer<'a, C> {
271    pub fn new(manager: &'a PostArchiverManager<C>) -> Self {
272        Self { manager }
273    }
274
275    /// Execute a SELECT query and deserialize all result rows into `Vec<Q>`.
276    pub fn fetch<Q: FromQuery>(
277        &self,
278        sql: &str,
279        params: Vec<Param>,
280    ) -> crate::error::Result<Vec<Q>> {
281        let mut stmt = self.manager.conn().prepare_cached(sql)?;
282        let params = params
283            .iter()
284            .map(|p| p.as_ref() as &dyn ToSql)
285            .collect::<Vec<_>>();
286        let rows = stmt.query_map(params.as_slice(), Q::from_row)?;
287        rows.collect::<Result<_, _>>().map_err(Into::into)
288    }
289
290    /// Execute a `SELECT COUNT(*)` query and return the number of matching rows.
291    pub fn count(&self, sql: &str, params: Vec<Param>) -> crate::error::Result<u64> {
292        let mut stmt = self.manager.conn().prepare_cached(sql)?;
293        let params = params
294            .iter()
295            .map(|p| p.as_ref() as &dyn ToSql)
296            .collect::<Vec<_>>();
297        Ok(stmt.query_row(params.as_slice(), |row| row.get(0))?)
298    }
299}
300
301// ── Paginate ─────────────────────────────────────────────────────────────────
302pub use paginate::*;
303pub mod paginate {
304    use crate::{
305        manager::PostArchiverConnection,
306        query::{Query, RawSql},
307    };
308
309    use super::{BaseFilter, FromQuery};
310
311    /// Trait that adds pagination support to query builders.
312    ///
313    /// Automatically implemented for all types that implement [`Query`](crate::query::Query).
314    pub trait Paginate: Sized {
315        /// Wrap this builder with `limit` (items per page) and `page` (0-based page index),
316        /// returning [`Paginated<Self>`] which appends `LIMIT limit OFFSET limit*page` on execution.
317        fn pagination(self, limit: u64, page: u64) -> Paginated<Self>;
318    }
319
320    impl<T: Query> Paginate for T {
321        fn pagination(self, limit: u64, page: u64) -> Paginated<Self> {
322            Paginated {
323                inner: self,
324                limit,
325                page,
326            }
327        }
328    }
329
330    /// Paginated wrapper produced by [`.pagination()`](Paginate::pagination).
331    ///
332    /// Appends `LIMIT … OFFSET …` to the inner builder's SQL on execution.
333    #[derive(Debug)]
334    pub struct Paginated<Q> {
335        inner: Q,
336        limit: u64,
337        page: u64,
338    }
339
340    impl<Q: Query> Query for Paginated<Q> {
341        type Wrapper<T> = Q::Wrapper<T>;
342        type Based = Q::Based;
343
344        fn query_with_context<T: FromQuery<Based = Self::Based>>(
345            self,
346            mut sql: RawSql<T>,
347        ) -> crate::error::Result<Self::Wrapper<T>> {
348            sql.limit_clause = Some([self.limit, self.limit * self.page]);
349            self.inner.query_with_context(sql)
350        }
351    }
352
353    impl<Q: BaseFilter> BaseFilter for Paginated<Q> {
354        type Based = Q::Based;
355
356        fn update_sql<T: FromQuery<Based = Self::Based>>(&self, sql: RawSql<T>) -> RawSql<T> {
357            self.inner.update_sql(sql)
358        }
359
360        fn queryer(&self) -> &crate::query::Queryer<'_, impl PostArchiverConnection> {
361            self.inner.queryer()
362        }
363    }
364}
365
366pub use countable::*;
367pub mod countable {
368    use serde::{Deserialize, Serialize};
369
370    use crate::{
371        manager::PostArchiverConnection,
372        query::{Query, RawSql},
373    };
374
375    use super::{BaseFilter, FromQuery};
376
377    /// Trait that adds total-count support to query builders.
378    ///
379    /// Automatically implemented for all types that implement [`Query`](crate::query::Query).
380    pub trait Countable: Sized {
381        /// Wrap this builder in [`WithTotal<Self>`]. When [`Query::query()`] is called,
382        /// an additional `COUNT(*)` query is executed and the result is placed in
383        /// the `total` field of the returned [`Totalled`].
384        fn with_total(self) -> WithTotal<Self> {
385            WithTotal { inner: self }
386        }
387    }
388
389    impl<T: Query> Countable for T {}
390
391    /// Wrapper produced by [`.with_total()`](Countable::with_total).
392    /// Executes an additional `COUNT(*)` query and places the result in [`Totalled::total`].
393    #[derive(Debug)]
394    pub struct WithTotal<Q> {
395        inner: Q,
396    }
397
398    impl<Q: Query + BaseFilter> Query for WithTotal<Q> {
399        type Wrapper<T> = Totalled<Q::Wrapper<T>>;
400        type Based = <Q as Query>::Based;
401
402        fn query_with_context<T: FromQuery<Based = Self::Based>>(
403            self,
404            sql: RawSql<T>,
405        ) -> crate::error::Result<Self::Wrapper<T>> {
406            let total = self.inner.count()?;
407            let items = self.inner.query_with_context(sql)?;
408            Ok(Totalled { items, total })
409        }
410    }
411
412    impl<T: BaseFilter> BaseFilter for WithTotal<T> {
413        type Based = T::Based;
414
415        fn update_sql<U: FromQuery<Based = Self::Based>>(&self, sql: RawSql<U>) -> RawSql<U> {
416            self.inner.update_sql(sql)
417        }
418
419        fn queryer(&self) -> &crate::query::Queryer<'_, impl PostArchiverConnection> {
420            self.inner.queryer()
421        }
422    }
423
424    /// Result container produced by `.with_total().query()`, holding both the query results
425    /// and the total count of rows matching the filter.
426    #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
427    pub struct Totalled<T> {
428        /// The results retrieved by this query (respecting any LIMIT/OFFSET).
429        pub items: T,
430        /// Total number of rows matching the filter, ignoring pagination.
431        pub total: u64,
432    }
433}
434
435pub use sortable::*;
436pub mod sortable {
437    use std::fmt::Display;
438
439    use crate::{
440        manager::PostArchiverConnection,
441        query::{Query, RawSql},
442    };
443
444    /// Trait that adds sorting support to query builders.
445    ///
446    /// Per-entity sort-field enums (e.g. `PostSort`, `AuthorSort`) are generated
447    /// automatically by the [`impl_sortable!`] macro.
448    pub trait Sortable: Sized {
449        /// The sortable field enum type, defined in each sub-module by [`impl_sortable!`].
450        type SortField;
451        /// Sort by the given field and direction, returning [`Sorted<Self, SortField>`].
452        fn sort(self, field: Self::SortField, dir: SortDir) -> Sorted<Self, Self::SortField> {
453            Sorted {
454                inner: self,
455                field,
456                dir,
457            }
458        }
459        /// Sort results randomly using `ORDER BY RANDOM()`.
460        fn sort_random(self) -> Sorted<Self, Random> {
461            Sorted {
462                inner: self,
463                field: Random,
464                dir: SortDir::Asc, // ignored
465            }
466        }
467    }
468
469    /// Sorting wrapper produced by [`.sort()`](Sortable::sort), holding the sort field and direction.
470    #[derive(Debug)]
471    pub struct Sorted<Q, F> {
472        inner: Q,
473        field: F,
474        dir: SortDir,
475    }
476
477    /// Marker type used by [`Sortable::sort_random()`] for random ordering.
478    #[derive(Debug)]
479    pub struct Random;
480
481    /// Sort direction used with [`.sort(field, dir)`](Sortable::sort).
482    #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
483    pub enum SortDir {
484        /// Ascending order (default).
485        #[default]
486        Asc,
487        /// Descending order.
488        Desc,
489    }
490
491    impl SortDir {
492        pub fn as_sql(self) -> &'static str {
493            match self {
494                SortDir::Asc => "ASC",
495                SortDir::Desc => "DESC",
496            }
497        }
498    }
499
500    impl<Q: Query, U: Display> Query for Sorted<Q, U> {
501        type Wrapper<T> = Q::Wrapper<T>;
502        type Based = Q::Based;
503        fn query_with_context<T: FromQuery<Based = Self::Based>>(
504            self,
505            mut sql: RawSql<T>,
506        ) -> crate::error::Result<Self::Wrapper<T>> {
507            sql.order_clause
508                .push(format!("{} {}", self.field, self.dir.as_sql()));
509            self.inner.query_with_context(sql)
510        }
511    }
512
513    impl<Q: Query> Query for Sorted<Q, Random> {
514        type Wrapper<T> = Q::Wrapper<T>;
515        type Based = Q::Based;
516        fn query_with_context<T: FromQuery<Based = Self::Based>>(
517            self,
518            mut sql: RawSql<T>,
519        ) -> crate::error::Result<Self::Wrapper<T>> {
520            sql.order_clause.push("RANDOM()".to_string());
521            self.inner.query_with_context(sql)
522        }
523    }
524
525    impl<Q: BaseFilter, U> BaseFilter for Sorted<Q, U> {
526        type Based = Q::Based;
527
528        fn update_sql<T: FromQuery<Based = Self::Based>>(&self, sql: RawSql<T>) -> RawSql<T> {
529            self.inner.update_sql(sql)
530        }
531
532        fn queryer(&self) -> &super::Queryer<'_, impl PostArchiverConnection> {
533            self.inner.queryer()
534        }
535    }
536
537    impl<T: Sortable> Sortable for WithTotal<T> {
538        type SortField = T::SortField;
539    }
540    impl<T: Sortable> Sortable for Paginated<T> {
541        type SortField = T::SortField;
542    }
543    impl<T: Sortable, U> Sortable for Sorted<T, U> {
544        type SortField = T::SortField;
545    }
546
547    #[macro_export]
548    macro_rules! impl_sortable {
549        ($query_type:ident ($sort_field_enum:ident) {
550            $($field:ident: $column:expr),*
551        }) => {
552            pub enum $sort_field_enum {
553                $($field),*
554            }
555
556            impl std::fmt::Display for $sort_field_enum {
557                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
558                    match self {
559                        $(Self::$field => write!(f, "{}", $column)),*
560                    }
561                }
562            }
563
564            impl<C> $crate::query::sortable::Sortable for $query_type<'_, C> {
565                type SortField = $sort_field_enum;
566            }
567        };
568    }
569    pub use impl_sortable;
570
571    use super::{countable::WithTotal, paginate::Paginated, BaseFilter, FromQuery};
572}