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