Skip to main content

aven_core/
query.rs

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