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/// 貼文的流式查詢構建器。
27///
28/// 透過 [`PostArchiverManager::posts()`] 取得。
29///
30/// # 可用過濾欄位
31/// - `ids`:依 `PostId` 陸列過濾。
32/// - `title`:依標題進行 `LIKE` 模糊匹配。
33/// - `source`:依來源字串進行 `LIKE` 模糊匹配。
34/// - `updated`:依最後更新時間進行範圍過濾。
35/// - `published`:依發布時間進行範圍過濾。
36/// - `platforms`:依所屬平台的 `PlatformId` 陸列過濾。
37/// - `tags`:展性匹配標籤,最小定包含透過 `post_tags` 關聯表指定的所有 `TagId`。
38/// - `authors`:展性匹配作者,結構同上,透過 `author_posts` 關聯表。
39/// - `collections`:展性匹配收藏集,結構同上,透過 `collection_posts` 關聯表。
40///
41/// # 範例
42/// ```no_run
43/// # use post_archiver::manager::PostArchiverManager;
44/// # use post_archiver::query::{SortDir, Sortable, Countable, Paginate, Query};
45/// # use post_archiver::query::post::PostSort;
46/// # let manager = PostArchiverManager::open_in_memory().unwrap();
47/// let posts = manager.posts()
48///     .sort(PostSort::Updated, SortDir::Desc)
49///     .pagination(20, 0)
50///     .query::<post_archiver::Post>()
51///     .unwrap();
52/// ```
53#[derive(Debug)]
54pub struct PostQuery<'a, C> {
55    queryer: Queryer<'a, C>,
56    pub ids: IdFilter<PostId>,
57    pub title: TextFilter,
58    pub source: TextFilter,
59    pub updated: DateFilter,
60    pub published: DateFilter,
61    pub platforms: IdFilter<PlatformId>,
62    pub tags: RelationshipsFilter<TagId>,
63    pub authors: RelationshipsFilter<AuthorId>,
64    pub collections: RelationshipsFilter<CollectionId>,
65}
66
67// ── Builder methods ───────────────────────────────────────────────────────────
68
69impl<'a, C: PostArchiverConnection> PostQuery<'a, C> {
70    pub fn new(manager: &'a PostArchiverManager<C>) -> Self {
71        PostQuery {
72            queryer: Queryer::new(manager),
73            ids: IdFilter::new("id"),
74            title: TextFilter::new("title"),
75            source: TextFilter::new("source"),
76            updated: DateFilter::new("updated"),
77            published: DateFilter::new("published"),
78            platforms: IdFilter::new("platform"),
79            tags: RelationshipsFilter::new("post_tags", "tag"),
80            authors: RelationshipsFilter::new("author_posts", "author"),
81            collections: RelationshipsFilter::new("collection_posts", "collection"),
82        }
83    }
84}
85
86// ── Trait impls ───────────────────────────────────────────────────────────────
87
88impl<C: PostArchiverConnection> BaseFilter for PostQuery<'_, C> {
89    type Based = Post;
90
91    fn update_sql<T: FromQuery<Based = Self::Based>>(&self, mut sql: RawSql<T>) -> RawSql<T> {
92        sql = self.ids.build_sql(sql);
93        sql = self.title.build_sql(sql);
94        sql = self.source.build_sql(sql);
95        sql = self.updated.build_sql(sql);
96        sql = self.published.build_sql(sql);
97        sql = self.platforms.build_sql(sql);
98        sql = self.authors.build_sql(sql);
99        sql = self.tags.build_sql(sql);
100        sql = self.collections.build_sql(sql);
101
102        sql
103    }
104
105    fn queryer(&self) -> &Queryer<'_, impl PostArchiverConnection> {
106        &self.queryer
107    }
108}
109
110impl<C: PostArchiverConnection> Query for PostQuery<'_, C> {
111    type Wrapper<U> = Vec<U>;
112    type Based = Post;
113
114    fn query_with_context<T: FromQuery<Based = Self::Based>>(
115        self,
116        sql: RawSql<T>,
117    ) -> crate::error::Result<Self::Wrapper<T>> {
118        let sql = self.update_sql(sql);
119        let (sql, params) = sql.build_sql();
120        self.queryer.fetch(&sql, params)
121    }
122}
123
124impl<C: PostArchiverConnection> PostArchiverManager<C> {
125    /// Entry point for the post query builder.
126    pub fn posts(&self) -> PostQuery<'_, C> {
127        PostQuery::new(self)
128    }
129
130    /// Fetch a single post by primary key. Returns `None` if not found.
131    pub fn get_post(&self, id: PostId) -> crate::error::Result<Option<Post>> {
132        let mut stmt = self
133            .conn()
134            .prepare_cached("SELECT * FROM posts WHERE id = ?")?;
135        Ok(stmt.query_row([id], Post::from_row).optional()?)
136    }
137
138    /// Look up a post ID by its `source` field.
139    pub fn find_post(&self, source: &str) -> crate::error::Result<Option<PostId>> {
140        let mut stmt = self
141            .conn()
142            .prepare_cached("SELECT id FROM posts WHERE source = ?")?;
143        Ok(stmt.query_row([source], |row| row.get(0)).optional()?)
144    }
145
146    /// Look up a post ID by its `source` field, but only if its `updated` timestamp
147    /// is not earlier than the given value.
148    pub fn find_post_with_updated(
149        &self,
150        source: &str,
151        updated: &DateTime<Utc>,
152    ) -> crate::error::Result<Option<PostId>> {
153        let mut stmt = self
154            .conn()
155            .prepare_cached("SELECT id FROM posts WHERE source = ? AND updated >= ?")?;
156        Ok(stmt
157            .query_row(params![source, updated], |row| row.get(0))
158            .optional()?)
159    }
160}