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