Skip to main content

aven_core/
query.rs

1use anyhow::{Result, bail};
2use chrono::{DateTime, Utc};
3
4use crate::choices::TaskStatus;
5use crate::db::Database;
6use crate::ids::{TaskId, WorkspaceId};
7use crate::recurrence::RecurrenceSeriesId;
8use crate::refs::DisplayRefContext;
9use crate::types::Task;
10
11mod database_stats;
12mod dependencies;
13mod details;
14mod doctor;
15pub mod fragments;
16mod hydration;
17mod projects;
18mod recent_actions;
19mod recurrence;
20mod related;
21mod search;
22mod sidebar;
23mod sorting;
24mod sync_history;
25mod tasks;
26#[cfg(test)]
27mod test_support;
28mod types;
29
30pub use database_stats::{DatabaseStats, DatabaseStatsPriorityCounts, DatabaseStatsStatusCounts};
31pub use dependencies::{TaskDependencyItem, TaskDependencySummary};
32pub(crate) use dependencies::{task_dependency_summary, task_dependency_summary_with_display_refs};
33pub use details::{TaskDetail, TaskDetailConflict};
34pub(crate) use details::{conflict_display_value, task_detail, task_detail_with_display_refs};
35pub use doctor::WorkspaceTaskCounts;
36pub(crate) use doctor::{unresolved_conflict_count, workspace_task_counts};
37pub(crate) use projects::list_project_items_in_workspace;
38pub(crate) use recent_actions::list_recent_actions_in_workspace;
39pub(crate) use recurrence::task_recurrence_summaries;
40pub use related::TaskRelatedLink;
41pub(crate) use related::{related_links_for_tasks, task_related_links};
42pub use search::{
43    SearchMatchedField, TaskSearchPreviewResultSet, TaskSearchQuery, TaskSearchResult,
44};
45pub(crate) use search::{
46    search_task_items_in_workspace, search_task_occurrence_items_in_workspace,
47    search_task_preview_set_in_workspace,
48};
49pub(crate) use sidebar::sidebar_counts_for_scope_in_workspace;
50pub use sync_history::SyncHistoryStats;
51pub(crate) use sync_history::sync_history_stats;
52pub(crate) use tasks::{
53    list_task_items_in_workspace, list_task_items_with_display_refs,
54    list_task_summary_items_in_workspace,
55};
56pub use types::RecentActionTarget;
57pub use types::{
58    AttachmentMetadata, EpicRollup, MetadataFilter, ProjectListItem, RecentActionItem,
59    RecurrenceCounts, RecurrenceHistoryEntry, RecurrenceHistoryKind, RecurrenceHistoryPage,
60    RecurrenceOccurrenceLink, RecurrenceReconciliation, RecurrenceSeriesConflict,
61    RecurrenceSeriesDetail, RecurrenceSeriesLifecycleFilter, RecurrenceSeriesListItem,
62    RecurrenceSeriesListQuery, RecurrenceSeriesSummary, RecurrenceTaskGroup, SidebarCounts,
63    SortDirection, TaskAvailabilityFilter, TaskDependencyLink, TaskFilters, TaskIdFilter,
64    TaskListItem, TaskNote, TaskQueryMode, TaskRecurrenceSummary, TaskSort,
65};
66
67pub const MAX_RECURRENCE_HISTORY_LIMIT: usize = 500;
68
69pub(crate) fn status_order(status: TaskStatus) -> u8 {
70    match status {
71        TaskStatus::Active => 0,
72        TaskStatus::Todo => 1,
73        TaskStatus::Inbox => 2,
74        TaskStatus::Backlog => 3,
75        TaskStatus::Done => 4,
76        TaskStatus::Canceled => 5,
77    }
78}
79
80fn validate_search_limit(limit: usize) -> Result<()> {
81    if limit == 0 {
82        bail!("error search-limit-invalid limit=0 min=1 hint=\"pass a limit of 1 or greater\"");
83    }
84    Ok(())
85}
86
87fn validate_recurrence_history_limit(limit: usize) -> Result<()> {
88    if !(1..=MAX_RECURRENCE_HISTORY_LIMIT).contains(&limit) {
89        bail!(
90            "error recurrence-history-limit-invalid limit={} min=1 max={} hint=\"pass a limit between 1 and {}\"",
91            limit,
92            MAX_RECURRENCE_HISTORY_LIMIT,
93            MAX_RECURRENCE_HISTORY_LIMIT
94        );
95    }
96    Ok(())
97}
98
99impl Database {
100    /// Reconciles recurrence projections before a report reads current state.
101    ///
102    /// The candidate count is bounded. Each changed series writes one atomic projection
103    /// transaction that can archive one superseded task and materialize one successor.
104    pub async fn reconcile_recurrence_reports_at(
105        &self,
106        workspace_id: &WorkspaceId,
107        at: DateTime<Utc>,
108    ) -> Result<RecurrenceReconciliation> {
109        let (workspace, candidates, incomplete) = {
110            let mut conn = self.acquire_reader().await?;
111            let workspace = crate::workspaces::workspace_for_id(&mut conn, workspace_id).await?;
112            let (candidates, incomplete) =
113                recurrence::recurrence_reconciliation_candidates(&mut conn, workspace_id, at)
114                    .await?;
115            (workspace, candidates, incomplete)
116        };
117        let mut report = RecurrenceReconciliation {
118            workspace_id: Some(workspace_id.clone()),
119            examined: candidates.len(),
120            incomplete,
121            ..RecurrenceReconciliation::default()
122        };
123        for series_id in candidates {
124            let result = self
125                .reconcile_recurrence_series(&workspace, &series_id, at)
126                .await?;
127            report.changed += usize::from(result.changed);
128            report.lifecycle_blocked += usize::from(result.lifecycle_blocked);
129        }
130        recurrence::validate_reconciliation(&report)?;
131        Ok(report)
132    }
133
134    pub async fn reconcile_recurrence_reports(
135        &self,
136        workspace_id: &WorkspaceId,
137    ) -> Result<RecurrenceReconciliation> {
138        let at = crate::ids::now_utc();
139        self.reconcile_recurrence_reports_at(workspace_id, at).await
140    }
141
142    pub async fn list_recurrence_series_view_at(
143        &self,
144        workspace_id: &WorkspaceId,
145        at: DateTime<Utc>,
146        query: RecurrenceSeriesListQuery,
147    ) -> Result<Vec<RecurrenceSeriesListItem>> {
148        self.reconcile_recurrence_reports_at(workspace_id, at)
149            .await?;
150        self.list_recurrence_series_view_from_current_projection(workspace_id, query)
151            .await
152    }
153
154    /// Reads the persisted recurrence projection as-is.
155    ///
156    /// Callers establish projection currency by reconciling the workspace for the report snapshot.
157    #[doc(hidden)]
158    pub async fn list_recurrence_series_view_from_current_projection(
159        &self,
160        workspace_id: &WorkspaceId,
161        query: RecurrenceSeriesListQuery,
162    ) -> Result<Vec<RecurrenceSeriesListItem>> {
163        let mut conn = self.acquire_reader().await?;
164        recurrence::list_recurrence_series_view(&mut conn, workspace_id, &query).await
165    }
166
167    pub async fn list_recurrence_series_view(
168        &self,
169        workspace_id: &WorkspaceId,
170        query: RecurrenceSeriesListQuery,
171    ) -> Result<Vec<RecurrenceSeriesListItem>> {
172        let at = crate::ids::now_utc();
173        self.list_recurrence_series_view_at(workspace_id, at, query)
174            .await
175    }
176
177    pub async fn list_recurrence_series_at(
178        &self,
179        workspace_id: &WorkspaceId,
180        at: DateTime<Utc>,
181    ) -> Result<Vec<RecurrenceSeriesSummary>> {
182        self.reconcile_recurrence_reports_at(workspace_id, at)
183            .await?;
184        let mut conn = self.acquire_reader().await?;
185        recurrence::list_recurrence_series(&mut conn, workspace_id, at).await
186    }
187
188    pub async fn recurrence_series_detail_at(
189        &self,
190        workspace_id: &WorkspaceId,
191        series_id: &RecurrenceSeriesId,
192        at: DateTime<Utc>,
193    ) -> Result<RecurrenceSeriesDetail> {
194        self.reconcile_recurrence_reports_at(workspace_id, at)
195            .await?;
196        self.recurrence_series_detail_at_from_current_projection(workspace_id, series_id, at)
197            .await
198    }
199
200    /// Reads the persisted recurrence projection as-is.
201    ///
202    /// Callers establish projection currency by reconciling the workspace for the report snapshot.
203    #[doc(hidden)]
204    pub async fn recurrence_series_detail_at_from_current_projection(
205        &self,
206        workspace_id: &WorkspaceId,
207        series_id: &RecurrenceSeriesId,
208        at: DateTime<Utc>,
209    ) -> Result<RecurrenceSeriesDetail> {
210        let mut conn = self.acquire_reader().await?;
211        recurrence::recurrence_series_detail(&mut conn, workspace_id, series_id, at).await
212    }
213
214    pub async fn recurrence_history_at(
215        &self,
216        workspace_id: &WorkspaceId,
217        series_id: &RecurrenceSeriesId,
218        at: DateTime<Utc>,
219        offset: usize,
220        limit: usize,
221    ) -> Result<RecurrenceHistoryPage> {
222        validate_recurrence_history_limit(limit)?;
223        self.reconcile_recurrence_reports_at(workspace_id, at)
224            .await?;
225        let mut conn = self.acquire_reader().await?;
226        recurrence::recurrence_history(&mut conn, workspace_id, series_id, at, offset, limit).await
227    }
228
229    pub async fn list_recurrence_series(
230        &self,
231        workspace_id: &WorkspaceId,
232    ) -> Result<Vec<RecurrenceSeriesSummary>> {
233        let at = crate::ids::now_utc();
234        self.list_recurrence_series_at(workspace_id, at).await
235    }
236
237    pub async fn recurrence_series_detail(
238        &self,
239        workspace_id: &WorkspaceId,
240        series_id: &RecurrenceSeriesId,
241    ) -> Result<RecurrenceSeriesDetail> {
242        let at = crate::ids::now_utc();
243        self.recurrence_series_detail_at(workspace_id, series_id, at)
244            .await
245    }
246
247    /// Reads the persisted recurrence projection as-is.
248    ///
249    /// Callers establish projection currency by reconciling the workspace for the report snapshot.
250    #[doc(hidden)]
251    pub async fn recurrence_series_detail_from_current_projection(
252        &self,
253        workspace_id: &WorkspaceId,
254        series_id: &RecurrenceSeriesId,
255    ) -> Result<RecurrenceSeriesDetail> {
256        let at = crate::ids::now_utc();
257        self.recurrence_series_detail_at_from_current_projection(workspace_id, series_id, at)
258            .await
259    }
260
261    pub async fn recurrence_history(
262        &self,
263        workspace_id: &WorkspaceId,
264        series_id: &RecurrenceSeriesId,
265        offset: usize,
266        limit: usize,
267    ) -> Result<RecurrenceHistoryPage> {
268        let at = crate::ids::now_utc();
269        self.recurrence_history_at(workspace_id, series_id, at, offset, limit)
270            .await
271    }
272
273    pub async fn list_project_items(
274        &self,
275        workspace_id: &WorkspaceId,
276    ) -> Result<Vec<ProjectListItem>> {
277        self.reconcile_recurrence_reports(workspace_id).await?;
278        self.list_project_items_from_current_projection(workspace_id)
279            .await
280    }
281
282    /// Reads the persisted recurrence projection as-is.
283    ///
284    /// Callers establish projection currency by reconciling the workspace for the report snapshot.
285    #[doc(hidden)]
286    pub async fn list_project_items_from_current_projection(
287        &self,
288        workspace_id: &WorkspaceId,
289    ) -> Result<Vec<ProjectListItem>> {
290        let mut conn = self.acquire_reader().await?;
291        list_project_items_in_workspace(&mut conn, workspace_id).await
292    }
293
294    pub async fn list_task_items(
295        &self,
296        workspace_id: &WorkspaceId,
297        filters: TaskFilters,
298        mode: TaskQueryMode,
299        sort: TaskSort,
300        direction: SortDirection,
301    ) -> Result<Vec<TaskListItem>> {
302        self.reconcile_recurrence_reports(workspace_id).await?;
303        self.list_task_items_from_current_projection(workspace_id, filters, mode, sort, direction)
304            .await
305    }
306
307    /// Reads the persisted recurrence projection as-is.
308    ///
309    /// Callers establish projection currency by reconciling the workspace for the report snapshot.
310    #[doc(hidden)]
311    pub async fn list_task_items_from_current_projection(
312        &self,
313        workspace_id: &WorkspaceId,
314        filters: TaskFilters,
315        mode: TaskQueryMode,
316        sort: TaskSort,
317        direction: SortDirection,
318    ) -> Result<Vec<TaskListItem>> {
319        let mut conn = self.acquire_reader().await?;
320        list_task_items_in_workspace(&mut conn, workspace_id, filters, mode, sort, direction).await
321    }
322
323    pub async fn list_task_summary_items(
324        &self,
325        workspace_id: &WorkspaceId,
326        filters: TaskFilters,
327        mode: TaskQueryMode,
328        sort: TaskSort,
329        direction: SortDirection,
330        limit: Option<usize>,
331    ) -> Result<Vec<TaskListItem>> {
332        self.reconcile_recurrence_reports(workspace_id).await?;
333        let mut conn = self.acquire_reader().await?;
334        list_task_summary_items_in_workspace(
335            &mut conn,
336            workspace_id,
337            filters,
338            mode,
339            sort,
340            direction,
341            limit,
342        )
343        .await
344    }
345
346    pub async fn sidebar_counts_for_scope(
347        &self,
348        workspace_id: &WorkspaceId,
349        project_key: Option<&str>,
350    ) -> Result<SidebarCounts> {
351        self.reconcile_recurrence_reports(workspace_id).await?;
352        self.sidebar_counts_for_scope_from_current_projection(workspace_id, project_key)
353            .await
354    }
355
356    /// Reads the persisted recurrence projection as-is.
357    ///
358    /// Callers establish projection currency by reconciling the workspace for the report snapshot.
359    #[doc(hidden)]
360    pub async fn sidebar_counts_for_scope_from_current_projection(
361        &self,
362        workspace_id: &WorkspaceId,
363        project_key: Option<&str>,
364    ) -> Result<SidebarCounts> {
365        let mut conn = self.acquire_reader().await?;
366        sidebar_counts_for_scope_in_workspace(&mut conn, workspace_id, project_key).await
367    }
368
369    pub async fn list_recent_actions(
370        &self,
371        workspace_id: &WorkspaceId,
372        project_scope: Option<&str>,
373    ) -> Result<Vec<RecentActionItem>> {
374        self.reconcile_recurrence_reports(workspace_id).await?;
375        self.list_recent_actions_from_current_projection(workspace_id, project_scope)
376            .await
377    }
378
379    /// Reads the persisted recurrence projection as-is.
380    ///
381    /// Callers establish projection currency by reconciling the workspace for the report snapshot.
382    #[doc(hidden)]
383    pub async fn list_recent_actions_from_current_projection(
384        &self,
385        workspace_id: &WorkspaceId,
386        project_scope: Option<&str>,
387    ) -> Result<Vec<RecentActionItem>> {
388        let mut conn = self.acquire_reader().await?;
389        list_recent_actions_in_workspace(&mut conn, workspace_id, project_scope).await
390    }
391
392    pub async fn search_task_items(
393        &self,
394        workspace_id: &WorkspaceId,
395        query: TaskSearchQuery,
396    ) -> Result<Vec<TaskSearchResult>> {
397        validate_search_limit(query.limit)?;
398        self.reconcile_recurrence_reports(workspace_id).await?;
399        let mut conn = self.acquire_reader().await?;
400        search_task_items_in_workspace(&mut conn, workspace_id, query).await
401    }
402
403    pub async fn search_task_occurrence_items(
404        &self,
405        workspace_id: &WorkspaceId,
406        query: TaskSearchQuery,
407    ) -> Result<Vec<TaskSearchResult>> {
408        validate_search_limit(query.limit)?;
409        self.reconcile_recurrence_reports(workspace_id).await?;
410        let mut conn = self.acquire_reader().await?;
411        search_task_occurrence_items_in_workspace(&mut conn, workspace_id, query).await
412    }
413
414    pub async fn search_task_preview_set(
415        &self,
416        workspace_id: &WorkspaceId,
417        query: TaskSearchQuery,
418    ) -> Result<TaskSearchPreviewResultSet> {
419        validate_search_limit(query.limit)?;
420        self.reconcile_recurrence_reports(workspace_id).await?;
421        self.search_task_preview_set_from_current_projection(workspace_id, query)
422            .await
423    }
424
425    /// Reads the persisted recurrence projection as-is.
426    ///
427    /// Callers establish projection currency by reconciling the workspace for the report snapshot.
428    #[doc(hidden)]
429    pub async fn search_task_preview_set_from_current_projection(
430        &self,
431        workspace_id: &WorkspaceId,
432        query: TaskSearchQuery,
433    ) -> Result<TaskSearchPreviewResultSet> {
434        validate_search_limit(query.limit)?;
435        let mut conn = self.acquire_reader().await?;
436        search_task_preview_set_in_workspace(&mut conn, workspace_id, query).await
437    }
438
439    pub async fn task_detail(&self, task: &Task) -> Result<TaskDetail> {
440        self.reconcile_recurrence_reports(&task.workspace_id)
441            .await?;
442        let mut conn = self.acquire_reader().await?;
443        task_detail(&mut conn, task).await
444    }
445
446    pub async fn task_detail_with_display_refs(
447        &self,
448        task: &Task,
449        display_refs: &DisplayRefContext,
450    ) -> Result<TaskDetail> {
451        self.reconcile_recurrence_reports(&task.workspace_id)
452            .await?;
453        let mut conn = self.acquire_reader().await?;
454        task_detail_with_display_refs(&mut conn, task, display_refs).await
455    }
456
457    pub async fn conflict_display_value(
458        &self,
459        workspace_id: &WorkspaceId,
460        field: &str,
461        value: &str,
462    ) -> Result<String> {
463        let mut conn = self.acquire_reader().await?;
464        conflict_display_value(&mut conn, workspace_id, field, value).await
465    }
466
467    pub async fn task_dependency_summary(
468        &self,
469        workspace_id: &WorkspaceId,
470        task_id: &TaskId,
471    ) -> Result<TaskDependencySummary> {
472        let mut conn = self.acquire_reader().await?;
473        task_dependency_summary(&mut conn, workspace_id, task_id).await
474    }
475
476    pub async fn task_related_links(
477        &self,
478        workspace_id: &WorkspaceId,
479        task_id: &TaskId,
480    ) -> Result<Vec<TaskRelatedLink>> {
481        let mut conn = self.acquire_reader().await?;
482        task_related_links(&mut conn, workspace_id, task_id, None).await
483    }
484
485    pub async fn workspace_task_counts(
486        &self,
487        workspace_id: &WorkspaceId,
488    ) -> Result<WorkspaceTaskCounts> {
489        let mut conn = self.acquire_reader().await?;
490        workspace_task_counts(&mut conn, workspace_id).await
491    }
492
493    pub async fn unresolved_conflict_count(&self) -> Result<i64> {
494        let mut conn = self.acquire_reader().await?;
495        unresolved_conflict_count(&mut conn).await
496    }
497
498    pub async fn sync_history_stats(&self) -> Result<SyncHistoryStats> {
499        let mut conn = self.acquire_reader().await?;
500        sync_history_stats(&mut conn).await
501    }
502
503    pub async fn database_stats(
504        &self,
505        workspace: &crate::workspaces::Workspace,
506    ) -> Result<DatabaseStats> {
507        let mut conn = self.acquire_reader().await?;
508        database_stats::database_stats(&mut conn, workspace).await
509    }
510}