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 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}