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