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