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