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