Skip to main content

post_archiver/query/
mod.rs

1//! Query builder module providing fluent read operations for all entities.
2//!
3//! # Entry points on [`PostArchiverManager`](crate::manager::PostArchiverManager)
4//!
5//! | Method | Builder | Returns |
6//! |--------|---------|---------|
7//! | `manager.posts()` | [`PostQuery`](post::PostQuery) | `Vec<Post>` / `PageResult<Post>` |
8//! | `manager.authors()` | [`AuthorQuery`](author::AuthorQuery) | `Vec<Author>` / `PageResult<Author>` |
9//! | `manager.tags()` | [`TagQuery`](tag::TagQuery) | `Vec<Tag>` / `PageResult<Tag>` |
10//! | `manager.platforms()` | [`PlatformQuery`](platform::PlatformQuery) | `Vec<Platform>` |
11//! | `manager.collections()` | [`CollectionQuery`](collection::CollectionQuery) | `Vec<Collection>` / `PageResult<Collection>` |
12//!
13//! # Chain Style
14//!
15//! `.pagination()` wraps the builder in [`Paginated`], and `.with_total()` further
16//! wraps it to include the total count.
17//!
18//! ```no_run
19//! # use post_archiver::manager::PostArchiverManager;
20//! # use post_archiver::query::{Countable, Paginate, Query};
21//! # let manager = PostArchiverManager::open_in_memory().unwrap();
22//! // Vec<Post>  (all matching, no LIMIT)
23//! let v = manager.posts().query::<post_archiver::Post>().unwrap();
24//!
25//! // Vec<Post>  (paginated)
26//! let v = manager.posts().pagination(20, 0).query::<post_archiver::Post>().unwrap();
27//!
28//! // PageResult<Post>  (paginated + total count)
29//! let p = manager.posts().pagination(20, 0).with_total().query::<post_archiver::Post>().unwrap();
30//! println!("{} total posts", p.total);
31//! ```
32
33pub mod author;
34pub mod collection;
35pub mod file_meta;
36pub mod filter;
37pub mod platform;
38pub mod post;
39pub mod tag;
40
41use cached::Cached;
42pub use countable::{Countable, Totalled};
43pub use paginate::Paginate;
44pub use sortable::{SortDir, Sortable};
45
46use std::{
47    fmt::{Debug, Display},
48    rc::Rc,
49};
50
51use rusqlite::ToSql;
52
53use crate::{
54    manager::{PostArchiverConnection, PostArchiverManager},
55    utils::macros::AsTable,
56};
57
58pub trait FromQuery: Sized {
59    type Based: AsTable;
60    fn select_sql() -> String;
61    fn from_row(row: &rusqlite::Row) -> Result<Self, rusqlite::Error>;
62}
63
64// ── Query Trait ───────────────────────────────────────────────────────────────
65
66/// Core query execution trait. Implementors provide `.query()` to fetch results.
67pub trait Query: Sized {
68    type Wrapper<T>;
69    type Based: AsTable;
70    /// Execute the query, returning matching items.
71    fn query_with_context<T: FromQuery<Based = Self::Based>>(
72        self,
73        sql: RawSql<T>,
74    ) -> crate::error::Result<Self::Wrapper<T>>;
75
76    fn query<T: FromQuery<Based = Self::Based>>(self) -> crate::error::Result<Self::Wrapper<T>> {
77        let sql: RawSql<T> = RawSql::new();
78        self.query_with_context(sql)
79    }
80}
81
82pub trait BaseFilter: Sized {
83    type Based: AsTable;
84    fn update_sql<T: FromQuery<Based = Self::Based>>(&self, sql: RawSql<T>) -> RawSql<T>;
85    fn queryer(&self) -> &Queryer<'_, impl PostArchiverConnection>;
86
87    fn count(&self) -> crate::error::Result<u64> {
88        let sql = RawSql::<Self::Based>::new();
89        let sql = self.update_sql(sql);
90        let (sql, params) = sql.build_count_sql();
91
92        let cache_key = (
93            sql.clone(),
94            params
95                .iter()
96                .map(|p| p.to_string())
97                .collect::<Vec<_>>()
98                .join(","),
99        );
100
101        let cached_count = self
102            .queryer()
103            .manager
104            .caches
105            .lock()
106            .unwrap()
107            .counts
108            .cache_get(&cache_key)
109            .cloned();
110        match cached_count {
111            Some(cached_count) => Ok(cached_count),
112            None => {
113                let count = self.queryer().count(&sql, params)?;
114                self.queryer()
115                    .manager
116                    .caches
117                    .lock()
118                    .unwrap()
119                    .counts
120                    .cache_set(cache_key, count);
121                Ok(count)
122            }
123        }
124    }
125}
126
127pub trait ToSqlAndEq: ToSql + Display {}
128impl<T: ToSql + Display> ToSqlAndEq for T {}
129
130pub type Param = Rc<dyn ToSqlAndEq>;
131
132#[derive(Default, Clone)]
133pub struct RawSql<T> {
134    pub where_clause: (Vec<String>, Vec<Param>),
135    pub order_clause: Vec<String>,
136    pub limit_clause: Option<[u64; 2]>,
137    _phantom: std::marker::PhantomData<T>,
138}
139
140impl<T> Debug for RawSql<T> {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        f.debug_struct("RawSql")
143            .field("where_clause", &self.where_clause.0)
144            .field("order_clause", &self.order_clause)
145            .field("limit_clause", &self.limit_clause)
146            .finish()
147    }
148}
149
150impl<T> RawSql<T> {
151    pub fn new() -> Self {
152        Self {
153            where_clause: (Vec::new(), Vec::new()),
154            order_clause: Vec::new(),
155            limit_clause: None,
156            _phantom: std::marker::PhantomData,
157        }
158    }
159
160    pub fn build_generic_sql(&self) -> (String, Vec<Param>) {
161        let mut params = self.where_clause.1.clone();
162        let where_sql = if self.where_clause.0.is_empty() {
163            "".to_string()
164        } else {
165            format!("WHERE {}", self.where_clause.0.join(" AND "))
166        };
167        let order_sql = if self.order_clause.is_empty() {
168            "".to_string()
169        } else {
170            format!("ORDER BY {}", self.order_clause.join(", "))
171        };
172        let limit_sql = if let Some([limit, offset]) = self.limit_clause {
173            params.push(Rc::new(limit));
174            params.push(Rc::new(offset));
175            "LIMIT ? OFFSET ?".to_string()
176        } else {
177            "".to_string()
178        };
179        (format!("{} {} {}", where_sql, order_sql, limit_sql), params)
180    }
181}
182
183impl<T: FromQuery> RawSql<T> {
184    pub fn build_sql(&self) -> (String, Vec<Param>) {
185        let (clause, params) = self.build_generic_sql();
186        let sql = format!("{} {}", T::select_sql(), clause);
187        (sql, params)
188    }
189
190    pub fn build_count_sql(&self) -> (String, Vec<Param>) {
191        let where_sql = if self.where_clause.0.is_empty() {
192            "".to_string()
193        } else {
194            format!("WHERE {}", self.where_clause.0.join(" AND "))
195        };
196        let sql = format!(
197            "SELECT COUNT(*) FROM {} {}",
198            T::Based::TABLE_NAME,
199            where_sql
200        );
201        (sql, self.where_clause.1.clone())
202    }
203}
204
205pub struct Queryer<'a, C> {
206    pub manager: &'a PostArchiverManager<C>,
207}
208
209impl<T> Debug for Queryer<'_, T> {
210    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211        f.debug_struct("Queryer").finish()
212    }
213}
214
215impl<'a, C: PostArchiverConnection> Queryer<'a, C> {
216    pub fn new(manager: &'a PostArchiverManager<C>) -> Self {
217        Self { manager }
218    }
219
220    pub fn fetch<Q: FromQuery>(
221        &self,
222        sql: &str,
223        params: Vec<Param>,
224    ) -> crate::error::Result<Vec<Q>> {
225        let mut stmt = self.manager.conn().prepare_cached(sql)?;
226        let params = params
227            .iter()
228            .map(|p| p.as_ref() as &dyn ToSql)
229            .collect::<Vec<_>>();
230        let rows = stmt.query_map(params.as_slice(), Q::from_row)?;
231        rows.collect::<Result<_, _>>().map_err(Into::into)
232    }
233
234    pub fn count(&self, sql: &str, params: Vec<Param>) -> crate::error::Result<u64> {
235        let mut stmt = self.manager.conn().prepare_cached(sql)?;
236        let params = params
237            .iter()
238            .map(|p| p.as_ref() as &dyn ToSql)
239            .collect::<Vec<_>>();
240        Ok(stmt.query_row(params.as_slice(), |row| row.get(0))?)
241    }
242}
243
244// ── Paginate ─────────────────────────────────────────────────────────────────
245pub use paginate::*;
246pub mod paginate {
247    use crate::{
248        manager::PostArchiverConnection,
249        query::{Query, RawSql},
250    };
251
252    use super::{BaseFilter, FromQuery};
253
254    pub trait Paginate: Sized {
255        fn pagination(self, limit: u64, page: u64) -> Paginated<Self>;
256    }
257
258    impl<T: Query> Paginate for T {
259        fn pagination(self, limit: u64, page: u64) -> Paginated<Self> {
260            Paginated {
261                inner: self,
262                limit,
263                page,
264            }
265        }
266    }
267
268    /// Paginated wrapper produced by [`.pagination()`](Paginate::pagination).
269    ///
270    /// Appends `LIMIT … OFFSET …` to the inner builder's SQL.
271    #[derive(Debug)]
272    pub struct Paginated<Q> {
273        inner: Q,
274        limit: u64,
275        page: u64,
276    }
277
278    impl<Q: Query> Query for Paginated<Q> {
279        type Wrapper<T> = Q::Wrapper<T>;
280        type Based = Q::Based;
281
282        fn query_with_context<T: FromQuery<Based = Self::Based>>(
283            self,
284            mut sql: RawSql<T>,
285        ) -> crate::error::Result<Self::Wrapper<T>> {
286            sql.limit_clause = Some([self.limit, self.limit * self.page]);
287            self.inner.query_with_context(sql)
288        }
289    }
290
291    impl<Q: BaseFilter> BaseFilter for Paginated<Q> {
292        type Based = Q::Based;
293
294        fn update_sql<T: FromQuery<Based = Self::Based>>(&self, sql: RawSql<T>) -> RawSql<T> {
295            self.inner.update_sql(sql)
296        }
297
298        fn queryer(&self) -> &crate::query::Queryer<'_, impl PostArchiverConnection> {
299            self.inner.queryer()
300        }
301    }
302}
303
304pub use countable::*;
305pub mod countable {
306    use serde::{Deserialize, Serialize};
307
308    use crate::{
309        manager::PostArchiverConnection,
310        query::{Query, RawSql},
311    };
312
313    use super::{BaseFilter, FromQuery};
314
315    pub trait Countable: Sized {
316        /// Chain: wrap result in [`PageResult`] including total count.
317        fn with_total(self) -> WithTotal<Self> {
318            WithTotal { inner: self }
319        }
320    }
321
322    impl<T: Query> Countable for T {}
323
324    #[derive(Debug)]
325    pub struct WithTotal<Q> {
326        inner: Q,
327    }
328
329    impl<Q: Query + BaseFilter> Query for WithTotal<Q> {
330        type Wrapper<T> = Totalled<Q::Wrapper<T>>;
331        type Based = <Q as Query>::Based;
332
333        fn query_with_context<T: FromQuery<Based = Self::Based>>(
334            self,
335            sql: RawSql<T>,
336        ) -> crate::error::Result<Self::Wrapper<T>> {
337            let total = self.inner.count()?;
338            let items = self.inner.query_with_context(sql)?;
339            Ok(Totalled { items, total })
340        }
341    }
342
343    impl<T: BaseFilter> BaseFilter for WithTotal<T> {
344        type Based = T::Based;
345
346        fn update_sql<U: FromQuery<Based = Self::Based>>(&self, sql: RawSql<U>) -> RawSql<U> {
347            self.inner.update_sql(sql)
348        }
349
350        fn queryer(&self) -> &crate::query::Queryer<'_, impl PostArchiverConnection> {
351            self.inner.queryer()
352        }
353    }
354
355    /// Result container for paginated queries (produced by `.with_total().query()`).
356    #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
357    pub struct Totalled<T> {
358        pub items: T,
359        /// Total number of rows matching the filter, ignoring pagination.
360        pub total: u64,
361    }
362}
363
364pub use sortable::*;
365pub mod sortable {
366    use std::fmt::Display;
367
368    use crate::{
369        manager::PostArchiverConnection,
370        query::{Query, RawSql},
371    };
372
373    pub trait Sortable: Sized {
374        type SortField;
375        fn sort(self, field: Self::SortField, dir: SortDir) -> Sorted<Self, Self::SortField> {
376            Sorted {
377                inner: self,
378                field,
379                dir,
380            }
381        }
382        fn sort_random(self) -> Sorted<Self, Random> {
383            Sorted {
384                inner: self,
385                field: Random,
386                dir: SortDir::Asc, // ignored
387            }
388        }
389    }
390
391    #[derive(Debug)]
392    pub struct Sorted<Q, F> {
393        inner: Q,
394        field: F,
395        dir: SortDir,
396    }
397
398    #[derive(Debug)]
399    pub struct Random;
400
401    /// Sort direction used with `.sort(field, dir)` builder methods.
402    #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
403    pub enum SortDir {
404        #[default]
405        Asc,
406        Desc,
407    }
408
409    impl SortDir {
410        pub fn as_sql(self) -> &'static str {
411            match self {
412                SortDir::Asc => "ASC",
413                SortDir::Desc => "DESC",
414            }
415        }
416    }
417
418    impl<Q: Query, U: Display> Query for Sorted<Q, U> {
419        type Wrapper<T> = Q::Wrapper<T>;
420        type Based = Q::Based;
421        fn query_with_context<T: FromQuery<Based = Self::Based>>(
422            self,
423            mut sql: RawSql<T>,
424        ) -> crate::error::Result<Self::Wrapper<T>> {
425            sql.order_clause
426                .push(format!("{} {}", self.field, self.dir.as_sql()));
427            self.inner.query_with_context(sql)
428        }
429    }
430
431    impl<Q: Query> Query for Sorted<Q, Random> {
432        type Wrapper<T> = Q::Wrapper<T>;
433        type Based = Q::Based;
434        fn query_with_context<T: FromQuery<Based = Self::Based>>(
435            self,
436            mut sql: RawSql<T>,
437        ) -> crate::error::Result<Self::Wrapper<T>> {
438            sql.order_clause.push("RANDOM()".to_string());
439            self.inner.query_with_context(sql)
440        }
441    }
442
443    impl<Q: BaseFilter, U> BaseFilter for Sorted<Q, U> {
444        type Based = Q::Based;
445
446        fn update_sql<T: FromQuery<Based = Self::Based>>(&self, sql: RawSql<T>) -> RawSql<T> {
447            self.inner.update_sql(sql)
448        }
449
450        fn queryer(&self) -> &super::Queryer<'_, impl PostArchiverConnection> {
451            self.inner.queryer()
452        }
453    }
454
455    impl<T: Sortable> Sortable for WithTotal<T> {
456        type SortField = T::SortField;
457    }
458    impl<T: Sortable> Sortable for Paginated<T> {
459        type SortField = T::SortField;
460    }
461    impl<T: Sortable, U> Sortable for Sorted<T, U> {
462        type SortField = T::SortField;
463    }
464
465    #[macro_export]
466    macro_rules! impl_sortable {
467        ($query_type:ident ($sort_field_enum:ident) {
468            $($field:ident: $column:expr),*
469        }) => {
470            pub enum $sort_field_enum {
471                $($field),*
472            }
473
474            impl std::fmt::Display for $sort_field_enum {
475                fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
476                    match self {
477                        $(Self::$field => write!(f, "{}", $column)),*
478                    }
479                }
480            }
481
482            impl<C> $crate::query::sortable::Sortable for $query_type<'_, C> {
483                type SortField = $sort_field_enum;
484            }
485        };
486    }
487    pub use impl_sortable;
488
489    use super::{countable::WithTotal, paginate::Paginated, BaseFilter, FromQuery};
490}