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