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 workspace_open_counts_from_current_projection(
281 &self,
282 ) -> Result<std::collections::HashMap<WorkspaceId, i64>> {
283 let mut conn = self.acquire_reader().await?;
284 projects::workspace_open_counts(&mut conn).await
285 }
286
287 pub async fn list_project_items(
288 &self,
289 workspace_id: &WorkspaceId,
290 ) -> Result<Vec<ProjectListItem>> {
291 self.reconcile_recurrence_reports(workspace_id).await?;
292 self.list_project_items_from_current_projection(workspace_id)
293 .await
294 }
295
296 #[doc(hidden)]
300 pub async fn list_project_items_from_current_projection(
301 &self,
302 workspace_id: &WorkspaceId,
303 ) -> Result<Vec<ProjectListItem>> {
304 let mut conn = self.acquire_reader().await?;
305 list_project_items_in_workspace(&mut conn, workspace_id).await
306 }
307
308 pub(crate) async fn list_base_tasks(
309 &self,
310 workspace_id: &WorkspaceId,
311 filters: TaskFilters,
312 sort: TaskSort,
313 direction: SortDirection,
314 ) -> Result<Vec<Task>> {
315 self.reconcile_recurrence_reports(workspace_id).await?;
316 let mut conn = self.acquire_reader().await?;
317 tasks::list_base_tasks_in_workspace(&mut conn, workspace_id, filters, sort, direction, None)
318 .await
319 }
320
321 pub async fn list_task_items(
322 &self,
323 workspace_id: &WorkspaceId,
324 filters: TaskFilters,
325 mode: TaskQueryMode,
326 sort: TaskSort,
327 direction: SortDirection,
328 ) -> Result<Vec<TaskListItem>> {
329 self.reconcile_recurrence_reports(workspace_id).await?;
330 self.list_task_items_from_current_projection(workspace_id, filters, mode, sort, direction)
331 .await
332 }
333
334 pub async fn list_bulk_update_task_items(
335 &self,
336 workspace_id: &WorkspaceId,
337 filters: TaskFilters,
338 mode: TaskQueryMode,
339 sort: TaskSort,
340 direction: SortDirection,
341 ) -> Result<Vec<TaskListItem>> {
342 self.reconcile_recurrence_reports(workspace_id).await?;
343 let mut conn = self.acquire_reader().await?;
344 list_bulk_update_task_items_in_workspace(
345 &mut conn,
346 workspace_id,
347 filters,
348 mode,
349 sort,
350 direction,
351 )
352 .await
353 }
354
355 #[doc(hidden)]
359 pub async fn list_task_items_from_current_projection(
360 &self,
361 workspace_id: &WorkspaceId,
362 filters: TaskFilters,
363 mode: TaskQueryMode,
364 sort: TaskSort,
365 direction: SortDirection,
366 ) -> Result<Vec<TaskListItem>> {
367 let mut conn = self.acquire_reader().await?;
368 list_task_items_in_workspace(&mut conn, workspace_id, filters, mode, sort, direction).await
369 }
370
371 pub async fn list_task_items_without_activity_from_current_projection(
372 &self,
373 workspace_id: &WorkspaceId,
374 filters: TaskFilters,
375 mode: TaskQueryMode,
376 sort: TaskSort,
377 direction: SortDirection,
378 ) -> Result<Vec<TaskListItem>> {
379 let mut conn = self.acquire_reader().await?;
380 list_task_items_without_activity_in_workspace(
381 &mut conn,
382 workspace_id,
383 filters,
384 mode,
385 sort,
386 direction,
387 )
388 .await
389 }
390
391 pub async fn list_task_summary_items(
392 &self,
393 workspace_id: &WorkspaceId,
394 filters: TaskFilters,
395 mode: TaskQueryMode,
396 sort: TaskSort,
397 direction: SortDirection,
398 limit: Option<usize>,
399 ) -> Result<Vec<TaskListItem>> {
400 self.reconcile_recurrence_reports(workspace_id).await?;
401 self.list_task_summary_items_from_current_projection(
402 workspace_id,
403 filters,
404 mode,
405 sort,
406 direction,
407 limit,
408 )
409 .await
410 }
411
412 #[doc(hidden)]
416 pub async fn list_task_summary_items_from_current_projection(
417 &self,
418 workspace_id: &WorkspaceId,
419 filters: TaskFilters,
420 mode: TaskQueryMode,
421 sort: TaskSort,
422 direction: SortDirection,
423 limit: Option<usize>,
424 ) -> Result<Vec<TaskListItem>> {
425 let mut conn = self.acquire_reader().await?;
426 list_task_summary_items_in_workspace(
427 &mut conn,
428 workspace_id,
429 filters,
430 mode,
431 sort,
432 direction,
433 limit,
434 )
435 .await
436 }
437
438 pub async fn sidebar_counts_for_scope(
439 &self,
440 workspace_id: &WorkspaceId,
441 project_key: Option<&str>,
442 ) -> Result<SidebarCounts> {
443 self.reconcile_recurrence_reports(workspace_id).await?;
444 self.sidebar_counts_for_scope_from_current_projection(workspace_id, project_key)
445 .await
446 }
447
448 #[doc(hidden)]
452 pub async fn sidebar_counts_for_scope_from_current_projection(
453 &self,
454 workspace_id: &WorkspaceId,
455 project_key: Option<&str>,
456 ) -> Result<SidebarCounts> {
457 let mut conn = self.acquire_reader().await?;
458 sidebar_counts_for_scope_in_workspace(&mut conn, workspace_id, project_key).await
459 }
460
461 pub async fn list_recent_actions(
462 &self,
463 workspace_id: &WorkspaceId,
464 project_scope: Option<&str>,
465 ) -> Result<Vec<RecentActionItem>> {
466 self.reconcile_recurrence_reports(workspace_id).await?;
467 self.list_recent_actions_from_current_projection(workspace_id, project_scope)
468 .await
469 }
470
471 #[doc(hidden)]
475 pub async fn list_recent_actions_from_current_projection(
476 &self,
477 workspace_id: &WorkspaceId,
478 project_scope: Option<&str>,
479 ) -> Result<Vec<RecentActionItem>> {
480 let mut conn = self.acquire_reader().await?;
481 list_recent_actions_in_workspace(&mut conn, workspace_id, project_scope).await
482 }
483
484 pub async fn task_activity_for_task(
485 &self,
486 workspace_id: &WorkspaceId,
487 task_id: &TaskId,
488 ) -> Result<Vec<RecentActionItem>> {
489 let mut conn = self.acquire_reader().await?;
490 Ok(task_activity_for_tasks_in_workspace(
491 &mut conn,
492 workspace_id,
493 std::slice::from_ref(task_id),
494 )
495 .await?
496 .remove(task_id)
497 .unwrap_or_default())
498 }
499
500 pub async fn search_task_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_items_in_workspace(&mut conn, workspace_id, query).await
509 }
510
511 pub async fn search_task_occurrence_items(
512 &self,
513 workspace_id: &WorkspaceId,
514 query: TaskSearchQuery,
515 ) -> Result<Vec<TaskSearchResult>> {
516 validate_search_limit(query.limit)?;
517 self.reconcile_recurrence_reports(workspace_id).await?;
518 let mut conn = self.acquire_reader().await?;
519 search_task_occurrence_items_in_workspace(&mut conn, workspace_id, query).await
520 }
521
522 pub async fn search_task_preview_set(
523 &self,
524 workspace_id: &WorkspaceId,
525 query: TaskSearchQuery,
526 ) -> Result<TaskSearchPreviewResultSet> {
527 validate_search_limit(query.limit)?;
528 self.reconcile_recurrence_reports(workspace_id).await?;
529 self.search_task_preview_set_from_current_projection(workspace_id, query)
530 .await
531 }
532
533 #[doc(hidden)]
537 pub async fn search_task_preview_set_from_current_projection(
538 &self,
539 workspace_id: &WorkspaceId,
540 query: TaskSearchQuery,
541 ) -> Result<TaskSearchPreviewResultSet> {
542 validate_search_limit(query.limit)?;
543 let mut conn = self.acquire_reader().await?;
544 search_task_preview_set_in_workspace(&mut conn, workspace_id, query).await
545 }
546
547 pub async fn task_detail(&self, task: &Task) -> Result<TaskDetail> {
548 self.reconcile_recurrence_reports(&task.workspace_id)
549 .await?;
550 let mut conn = self.acquire_reader().await?;
551 task_detail(&mut conn, task).await
552 }
553
554 pub async fn task_detail_with_display_refs(
555 &self,
556 task: &Task,
557 display_refs: &DisplayRefContext,
558 ) -> Result<TaskDetail> {
559 self.reconcile_recurrence_reports(&task.workspace_id)
560 .await?;
561 let mut conn = self.acquire_reader().await?;
562 task_detail_with_display_refs(&mut conn, task, display_refs).await
563 }
564
565 pub async fn conflict_display_value(
566 &self,
567 workspace_id: &WorkspaceId,
568 field: &str,
569 value: &str,
570 ) -> Result<String> {
571 let mut conn = self.acquire_reader().await?;
572 conflict_display_value(&mut conn, workspace_id, field, value).await
573 }
574
575 pub async fn task_dependency_summary(
576 &self,
577 workspace_id: &WorkspaceId,
578 task_id: &TaskId,
579 ) -> Result<TaskDependencySummary> {
580 let mut conn = self.acquire_reader().await?;
581 task_dependency_summary(&mut conn, workspace_id, task_id).await
582 }
583
584 pub async fn task_related_links(
585 &self,
586 workspace_id: &WorkspaceId,
587 task_id: &TaskId,
588 ) -> Result<Vec<TaskRelatedLink>> {
589 let mut conn = self.acquire_reader().await?;
590 task_related_links(&mut conn, workspace_id, task_id, None).await
591 }
592
593 pub async fn workspace_task_counts(
594 &self,
595 workspace_id: &WorkspaceId,
596 ) -> Result<WorkspaceTaskCounts> {
597 let mut conn = self.acquire_reader().await?;
598 workspace_task_counts(&mut conn, workspace_id).await
599 }
600
601 pub async fn unresolved_conflict_count(&self) -> Result<i64> {
602 let mut conn = self.acquire_reader().await?;
603 unresolved_conflict_count(&mut conn).await
604 }
605
606 pub async fn unresolved_task_conflict_count_in_workspace(
607 &self,
608 workspace_id: &WorkspaceId,
609 ) -> Result<i64> {
610 let mut conn = self.acquire_reader().await?;
611 unresolved_task_conflict_count_in_workspace(&mut conn, workspace_id).await
612 }
613
614 pub async fn sync_history_stats(&self) -> Result<SyncHistoryStats> {
615 let mut conn = self.acquire_reader().await?;
616 sync_history_stats(&mut conn).await
617 }
618
619 pub async fn database_stats(
620 &self,
621 workspace: &crate::workspaces::Workspace,
622 ) -> Result<DatabaseStats> {
623 let mut conn = self.acquire_reader().await?;
624 database_stats::database_stats(&mut conn, workspace).await
625 }
626}