Skip to main content

post_archiver/query/
post.rs

1//! Post query builder and related point-query helpers.
2
3use chrono::{DateTime, Utc};
4use rusqlite::{params, OptionalExtension};
5
6use crate::{
7    manager::{PostArchiverConnection, PostArchiverManager},
8    AuthorId, CollectionId, PlatformId, Post, PostId, TagId,
9};
10
11use super::{
12    filter::{DateFilter, IdFilter, RelationshipsFilter, TextFilter},
13    sortable::impl_sortable,
14    BaseFilter, FromQuery, Query, Queryer, RawSql,
15};
16
17impl_sortable!(PostQuery(PostSort) {
18    Id: "id",
19    Updated: "updated",
20    Published: "published",
21    Title: "title"
22});
23
24// ── Builder ───────────────────────────────────────────────────────────────────
25
26/// Fluent query builder for posts.  Obtained via [`PostArchiverManager::posts()`].
27///
28/// # Available filter fields
29/// - `ids`: filter by a set of [`PostId`] values.
30/// - `title`: fuzzy match on the title.
31/// - `source`: fuzzy match on the post source.
32/// - `updated`:filter by last-updated timestamp range.
33/// - `published`:filter by published timestamp range.
34/// - `platforms`:filter by a set of [`PlatformId`] values.
35/// - `tags`:filter by a set of [`TagId`] values, through the `post_tags` relation table.
36/// - `authors`:filter by a set of [`AuthorId`] values, through the `author_posts` relation table。
37/// - `collections`:filter by a set of [`CollectionId`] values, through the `collection_posts` relation table.
38///
39/// # Example
40/// ```no_run
41/// # use post_archiver::manager::PostArchiverManager;
42/// # use post_archiver::query::{SortDir, Sortable, Countable, Paginate, Query};
43/// # use post_archiver::query::post::PostSort;
44/// # let manager = PostArchiverManager::open_in_memory().unwrap();
45/// let posts = manager.posts()
46///     .sort(PostSort::Updated, SortDir::Desc)
47///     .pagination(20, 0)
48///     .query::<post_archiver::Post>()
49///     .unwrap();
50/// ```
51#[derive(Debug)]
52pub struct PostQuery<'a, C> {
53    queryer: Queryer<'a, C>,
54    pub ids: IdFilter<PostId>,
55    pub title: TextFilter,
56    pub source: TextFilter,
57    pub updated: DateFilter,
58    pub published: DateFilter,
59    pub platforms: IdFilter<PlatformId>,
60    pub tags: RelationshipsFilter<TagId>,
61    pub authors: RelationshipsFilter<AuthorId>,
62    pub collections: RelationshipsFilter<CollectionId>,
63}
64
65// ── Builder methods ───────────────────────────────────────────────────────────
66
67impl<'a, C: PostArchiverConnection> PostQuery<'a, C> {
68    pub fn new(manager: &'a PostArchiverManager<C>) -> Self {
69        PostQuery {
70            queryer: Queryer::new(manager),
71            ids: IdFilter::new("id"),
72            title: TextFilter::new("title"),
73            source: TextFilter::new("source"),
74            updated: DateFilter::new("updated"),
75            published: DateFilter::new("published"),
76            platforms: IdFilter::new("platform"),
77            tags: RelationshipsFilter::new("post_tags", "tag"),
78            authors: RelationshipsFilter::new("author_posts", "author"),
79            collections: RelationshipsFilter::new("collection_posts", "collection"),
80        }
81    }
82}
83
84// ── Trait impls ───────────────────────────────────────────────────────────────
85
86impl<C: PostArchiverConnection> BaseFilter for PostQuery<'_, C> {
87    type Based = Post;
88
89    fn update_sql<T: FromQuery<Based = Self::Based>>(&self, mut sql: RawSql<T>) -> RawSql<T> {
90        sql = self.ids.build_sql(sql);
91        sql = self.title.build_sql(sql);
92        sql = self.source.build_sql(sql);
93        sql = self.updated.build_sql(sql);
94        sql = self.published.build_sql(sql);
95        sql = self.platforms.build_sql(sql);
96        sql = self.authors.build_sql(sql);
97        sql = self.tags.build_sql(sql);
98        sql = self.collections.build_sql(sql);
99
100        sql
101    }
102
103    fn queryer(&self) -> &Queryer<'_, impl PostArchiverConnection> {
104        &self.queryer
105    }
106}
107
108impl<C: PostArchiverConnection> Query for PostQuery<'_, C> {
109    type Wrapper<U> = Vec<U>;
110    type Based = Post;
111
112    fn query_with_context<T: FromQuery<Based = Self::Based>>(
113        self,
114        sql: RawSql<T>,
115    ) -> crate::error::Result<Self::Wrapper<T>> {
116        let sql = self.update_sql(sql);
117        let (sql, params) = sql.build_sql();
118        self.queryer.fetch(&sql, params)
119    }
120}
121
122impl<C: PostArchiverConnection> PostArchiverManager<C> {
123    /// Entry point for the post query builder.
124    pub fn posts(&self) -> PostQuery<'_, C> {
125        PostQuery::new(self)
126    }
127
128    /// Fetch a single post by primary key. Returns `None` if not found.
129    pub fn get_post(&self, id: PostId) -> crate::error::Result<Option<Post>> {
130        let mut stmt = self
131            .conn()
132            .prepare_cached("SELECT * FROM posts WHERE id = ?")?;
133        Ok(stmt.query_row([id], Post::from_row).optional()?)
134    }
135
136    /// Look up a post ID by its `source` field.
137    pub fn find_post(&self, source: &str) -> crate::error::Result<Option<PostId>> {
138        let mut stmt = self
139            .conn()
140            .prepare_cached("SELECT id FROM posts WHERE source = ?")?;
141        Ok(stmt.query_row([source], |row| row.get(0)).optional()?)
142    }
143
144    /// Look up a post ID by its `source` field, but only if its `updated` timestamp
145    /// is not earlier than the given value.
146    pub fn find_post_with_updated(
147        &self,
148        source: &str,
149        updated: &DateTime<Utc>,
150    ) -> crate::error::Result<Option<PostId>> {
151        let mut stmt = self
152            .conn()
153            .prepare_cached("SELECT id FROM posts WHERE source = ? AND updated >= ?")?;
154        Ok(stmt
155            .query_row(params![source, updated], |row| row.get(0))
156            .optional()?)
157    }
158}