kasl/db/tasks.rs
1//! Task management database operations.
2//!
3//! Provides core functionality for managing tasks within the kasl application.
4//! Handles all database interactions for task creation, modification, deletion,
5//! and retrieval with support for advanced filtering, tagging, and relationship management.
6//!
7//! ## Features
8//!
9//! - **CRUD Operations**: Complete Create, Read, Update, Delete functionality
10//! - **Advanced Filtering**: Multi-criteria task querying with date, completion, and tag filters
11//! - **Batch Operations**: Efficient bulk deletion and modification operations
12//! - **Tag Integration**: Seamless integration with the tagging system for categorization
13//!
14//! ## Usage
15//!
16//! ```rust,no_run
17//! # fn main() -> anyhow::Result<()> {
18//! use kasl::db::tasks::Tasks;
19//! use kasl::libs::task::Task;
20//!
21//! let mut tasks = Tasks::new()?;
22//! let task = Task::new("Review code", "Check PR #123", Some(75));
23//! tasks.insert(&task)?;
24//! # Ok(())
25//! # }
26//! ```
27
28use super::db::Db;
29use crate::libs::messages::Message;
30use crate::libs::task::{Task, TaskFilter};
31use crate::msg_error_anyhow;
32use anyhow::Result;
33use rusqlite::{Connection, Statement, ToSql, params};
34use std::vec;
35
36/// SQL schema definition for the tasks table.
37///
38/// Defines the complete structure for storing task information with support for:
39/// - Unique identification and hierarchical relationships
40/// - Temporal tracking with automatic timestamp generation
41/// - Progress monitoring through completion percentages
42/// - Flexible content storage with optional descriptions
43/// - Search and visibility control mechanisms
44const SCHEMA_TASKS: &str = "CREATE TABLE IF NOT EXISTS tasks (
45 id INTEGER NOT NULL PRIMARY KEY,
46 task_id INTEGER NOT NULL ON CONFLICT REPLACE DEFAULT 0,
47 timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
48 name TEXT NOT NULL,
49 comment TEXT,
50 completeness INTEGER NOT NULL ON CONFLICT REPLACE DEFAULT 100,
51 excluded_from_search BOOLEAN NOT NULL ON CONFLICT REPLACE DEFAULT FALSE
52);";
53
54/// Insert a new task with full field specification and return the assigned ID.
55///
56/// Creates a task record with automatic timestamp generation and immediate
57/// ID return for further operations. Supports all task properties including
58/// hierarchical relationships and search visibility controls.
59const INSERT_TASK: &str = "INSERT INTO tasks (task_id, timestamp, name, comment, completeness, excluded_from_search) VALUES
60 (?, datetime(CURRENT_TIMESTAMP, 'localtime'), ?, ?, ?, ?) RETURNING id";
61
62/// Update the parent task relationship for an existing task.
63///
64/// Establishes or modifies hierarchical task relationships by setting
65/// the task_id field to reference another task as the parent.
66const UPDATE_TASK_ID: &str = "UPDATE tasks SET task_id = ? WHERE id = ?";
67
68/// Base query for selecting all task fields.
69///
70/// Foundation query that retrieves complete task records with all
71/// properties. Used as the base for more specific filtering queries.
72const SELECT_TASKS: &str = "SELECT * FROM tasks";
73
74/// Date-based filtering clause for tasks created on a specific date.
75///
76/// Filters tasks by creation date using local timezone conversion
77/// to ensure accurate date matching across different time zones.
78const WHERE_DATE: &str = "WHERE date(timestamp) = date(?1)";
79
80/// ID-based filtering clause for retrieving specific tasks.
81///
82/// Dynamically constructed clause for filtering tasks by a list
83/// of specific id values using IN operator.
84const WHERE_ID_IN: &str = "WHERE id IN";
85
86/// Complex filtering for incomplete tasks from recent periods.
87///
88/// Sophisticated query that finds tasks meeting multiple criteria:
89/// - Completion status less than 100%
90/// - Not already present in today's task list
91/// - Represents the latest completion state from the past 15 days
92/// - Groups by task_id to avoid duplicates
93const WHERE_INCOMPLETE: &str = "WHERE
94 completeness < 100 AND
95 task_id NOT IN (SELECT task_id FROM tasks WHERE DATE(timestamp) = DATE('now')) AND
96 (task_id, completeness) IN (SELECT task_id, MAX(completeness) FROM tasks
97 WHERE DATE(timestamp) BETWEEN datetime(CURRENT_TIMESTAMP, 'localtime', '-15 day') AND datetime(CURRENT_TIMESTAMP, 'localtime', '-1 day')
98 GROUP BY task_id)
99 GROUP BY task_id";
100
101/// Tag-based filtering for tasks associated with a specific tag.
102///
103/// Joins tasks with the tag system to filter by tag name,
104/// enabling categorical task retrieval and organization.
105const WHERE_TAG: &str = "WHERE id IN (SELECT task_id FROM task_tags tt JOIN tags t ON tt.tag_id = t.id WHERE t.name = ?1)";
106
107/// Multiple tag filtering for tasks associated with any of the specified tags.
108///
109/// Extends single tag filtering to support multiple tag names,
110/// allowing for more flexible task categorization and retrieval.
111const WHERE_TAGS: &str = "WHERE id IN (SELECT task_id FROM task_tags tt JOIN tags t ON tt.tag_id = t.id WHERE t.name IN";
112
113/// Delete a single task record by its unique identifier.
114///
115/// Permanently removes a task from the database. Note that this will
116/// also cascade to remove task-tag relationships due to foreign key constraints.
117const DELETE_TASK: &str = "DELETE FROM tasks WHERE id = ?";
118
119/// Bulk deletion query for removing multiple tasks efficiently.
120///
121/// Dynamically constructed query for deleting multiple tasks in a single
122/// database operation, improving performance over individual deletions.
123const DELETE_TASKS_BY_IDS: &str = "DELETE FROM tasks WHERE id IN";
124
125/// Existence check query for validating task presence.
126///
127/// Efficiently determines if a task with the specified ID exists
128/// without retrieving the full task data.
129const SELECT_COUNT_BY_ID: &str = "SELECT COUNT(*) FROM tasks WHERE id = ?";
130
131/// Update query for modifying existing task properties.
132///
133/// Updates the core task properties (name, comment, completeness)
134/// while preserving the task ID, timestamp, and relationships.
135const UPDATE_TASK: &str = "UPDATE tasks SET name = ?, comment = ?, completeness = ? WHERE id = ?";
136
137/// Database interface for comprehensive task management operations.
138///
139/// The `Tasks` struct provides a high-level API for all task-related database
140/// operations, managing connections, transactions, and result processing.
141/// It maintains state for the most recently inserted task ID to support
142/// method chaining and immediate post-creation operations.
143///
144/// ## Architecture
145///
146/// - **Connection Management**: Direct SQLite connection for optimal performance
147/// - **State Tracking**: Maintains last insertion ID for operation chaining
148/// - **Error Handling**: Comprehensive error propagation and message generation
149/// - **Transaction Support**: Implicit transaction handling for data consistency
150///
151/// ## Performance Considerations
152///
153/// - Prepared statements are used for frequently executed queries
154/// - Bulk operations are optimized for large dataset manipulation
155/// - Index-aware query construction for efficient filtering
156/// - Tag integration is lazy-loaded to minimize overhead
157#[derive(Debug)]
158pub struct Tasks {
159 /// Direct SQLite database connection for task operations.
160 ///
161 /// Provides transactional access to the tasks table and related
162 /// structures. Connection is managed internally and provides
163 /// optimal performance for task-specific operations.
164 pub conn: Connection,
165
166 /// Identifier of the most recently inserted task.
167 ///
168 /// Maintained automatically by insertion operations to support
169 /// method chaining and immediate post-creation tasks like
170 /// tag association or hierarchical relationship establishment.
171 pub id: Option<i32>,
172}
173
174impl Tasks {
175 /// Creates a new Tasks manager and initializes the database schema.
176 ///
177 /// This constructor establishes a database connection, ensures the tasks
178 /// table schema is properly initialized, and prepares the manager for
179 /// task operations. Schema creation is idempotent and safe for repeated calls.
180 ///
181 /// # Returns
182 ///
183 /// Returns a new `Tasks` instance ready for task management operations,
184 /// or an error if database initialization fails.
185 ///
186 /// # Example
187 ///
188 /// ```rust,no_run
189 /// # fn main() -> anyhow::Result<()> {
190 /// use kasl::db::tasks::Tasks;
191 ///
192 /// let mut tasks = Tasks::new()?;
193 /// // Ready for task operations
194 /// # Ok(())
195 /// # }
196 /// ```
197 ///
198 /// # Errors
199 ///
200 /// Returns an error if:
201 /// - Database connection cannot be established
202 /// - Schema creation fails due to permissions or corruption
203 /// - Migration system encounters errors during table setup
204 pub fn new() -> Result<Self> {
205 let db = Db::new()?;
206
207 // Initialize the tasks table schema
208 db.conn.execute(SCHEMA_TASKS, [])?;
209
210 Ok(Self { conn: db.conn, id: None })
211 }
212
213 /// Inserts a new task into the database and returns a mutable reference for chaining.
214 ///
215 /// Creates a new task record with automatic ID assignment and timestamp generation.
216 /// The task's completion status, parent relationships, and search visibility are
217 /// all properly configured during insertion. The assigned ID is stored internally
218 /// for subsequent operations.
219 ///
220 /// ## Automatic Field Handling
221 ///
222 /// - **ID Assignment**: Database automatically assigns unique primary key
223 /// - **Timestamp**: Current local time is set as creation timestamp
224 /// - **Validation**: Required fields are validated before insertion
225 /// - **Defaults**: Missing optional fields receive appropriate default values
226 ///
227 /// # Arguments
228 ///
229 /// * `task` - Task object containing the properties to insert
230 ///
231 /// # Returns
232 ///
233 /// Returns a mutable reference to self for method chaining, allowing
234 /// immediate follow-up operations like tag association or updates.
235 ///
236 /// # Example
237 ///
238 /// ```rust,no_run
239 /// # fn main() -> anyhow::Result<()> {
240 /// use kasl::db::tasks::Tasks;
241 /// use kasl::libs::task::Task;
242 ///
243 /// let mut tasks = Tasks::new()?;
244 /// let task = Task::new("Code review", "Review PR #123", Some(50));
245 /// tasks.insert(&task)?
246 /// .update_id()?; // Method chaining
247 /// # Ok(())
248 /// # }
249 /// ```
250 ///
251 /// # Database Effects
252 ///
253 /// - Creates new record in tasks table
254 /// - Triggers any associated database triggers
255 /// - Updates internal state with new task ID
256 /// - Maintains referential integrity with tag system
257 pub fn insert(&mut self, task: &Task) -> Result<&mut Self> {
258 // Execute insertion query and capture the returned ID
259 self.id = Some(self.conn.query_row(
260 INSERT_TASK,
261 params![task.task_id, task.name, task.comment, task.completeness, task.excluded_from_search],
262 |row| row.get(0),
263 )?);
264
265 Ok(self)
266 }
267
268 /// Updates the parent task relationship for the most recently inserted task.
269 ///
270 /// This method sets the task_id field to establish hierarchical relationships
271 /// between tasks. It operates on the task ID stored from the most recent
272 /// insertion, making it ideal for immediate post-creation relationship setup.
273 ///
274 /// ## Hierarchical Task Support
275 ///
276 /// - **Parent-Child Relationships**: Link tasks in hierarchical structures
277 /// - **Project Organization**: Group related tasks under parent tasks
278 /// - **Dependency Tracking**: Establish task dependencies and workflows
279 /// - **Nested Task Management**: Support for multi-level task hierarchies
280 ///
281 /// # Returns
282 ///
283 /// Returns a mutable reference to self for continued method chaining,
284 /// or an error if no recent insertion ID is available.
285 ///
286 /// # Example
287 ///
288 /// ```rust,no_run
289 /// # use kasl::db::tasks::Tasks;
290 /// use kasl::libs::task::Task;
291 ///
292 /// # fn main() -> anyhow::Result<()> {
293 /// let mut tasks = Tasks::new()?;
294 /// let subtask = Task::new("Subtask", "Part of larger task", Some(0));
295 /// tasks.insert(&subtask)?
296 /// .update_id()?; // Set task_id to reference itself or parent
297 /// # Ok(())
298 /// # }
299 /// ```
300 ///
301 /// # Errors
302 ///
303 /// Returns an error if:
304 /// - No recent insertion ID is available (call after insert)
305 /// - Database update operation fails
306 /// - Referential integrity constraints are violated
307 pub fn update_id(&mut self) -> Result<&mut Self> {
308 self.conn.execute(UPDATE_TASK_ID, params![self.id, self.id])?;
309 Ok(self)
310 }
311
312 /// Retrieves the most recently inserted task as a vector.
313 ///
314 /// This convenience method fetches the complete task record for the
315 /// most recently inserted task, including all associated tags and
316 /// relationships. Useful for immediate verification of insertion results.
317 ///
318 /// # Returns
319 ///
320 /// Returns a vector containing the single most recent task, or an error
321 /// if no recent insertion ID is available or the task cannot be found.
322 ///
323 /// # Example
324 ///
325 /// ```rust,no_run
326 /// # use kasl::db::tasks::Tasks;
327 /// use kasl::libs::task::Task;
328 ///
329 /// # fn main() -> anyhow::Result<()> {
330 /// let mut tasks = Tasks::new()?;
331 /// let task = Task::new("New task", "Description", Some(100));
332 /// let inserted_tasks = tasks.insert(&task)?
333 /// .get()?; // Retrieve the inserted task
334 /// # Ok(())
335 /// # }
336 /// ```
337 ///
338 /// # Error Conditions
339 ///
340 /// - No recent insertion ID available
341 /// - Task was deleted after insertion
342 /// - Database access failures
343 pub fn get(&mut self) -> Result<Vec<Task>> {
344 let id = self.id.ok_or_else(|| msg_error_anyhow!(Message::NoIdSet))?;
345 self.fetch(TaskFilter::ByIds(vec![id]))
346 }
347
348 /// Fetches tasks based on sophisticated filtering criteria.
349 ///
350 /// This is the primary query method that supports all task filtering scenarios
351 /// through a unified interface. It dynamically constructs SQL queries based on
352 /// the provided filter, executes them efficiently, and enriches results with
353 /// associated tag information.
354 ///
355 /// ## Supported Filters
356 ///
357 /// - **All**: Retrieves every task in the database without restrictions
358 /// - **Date**: Tasks created on a specific date (local timezone)
359 /// - **Incomplete**: Tasks with progress < 100% from recent periods
360 /// - **ByIds**: Direct lookup of tasks by their unique identifiers
361 /// - **ByTag**: Tasks associated with a specific tag name
362 /// - **ByTags**: Tasks associated with any of multiple tag names
363 ///
364 /// ## Query Optimization
365 ///
366 /// The method uses prepared statements and parameterized queries for security
367 /// and performance. Complex filters like "Incomplete" use sophisticated SQL
368 /// to efficiently identify the latest completion status for each task.
369 ///
370 /// ## Tag Integration
371 ///
372 /// All returned tasks are automatically enriched with their associated tag
373 /// information through a secondary query. This provides complete task context
374 /// without requiring separate tag lookups.
375 ///
376 /// # Arguments
377 ///
378 /// * `filter` - The filtering criteria to apply when retrieving tasks
379 ///
380 /// # Returns
381 ///
382 /// Returns a vector of tasks matching the filter criteria, with complete
383 /// tag information included. Empty vector if no tasks match the filter.
384 ///
385 /// # Example
386 ///
387 /// ```rust,no_run
388 /// # fn main() -> anyhow::Result<()> {
389 /// use kasl::db::tasks::Tasks;
390 /// use kasl::libs::task::TaskFilter;
391 /// use chrono::Local;
392 ///
393 /// let mut tasks = Tasks::new()?;
394 ///
395 /// // Get all tasks
396 /// let all_tasks = tasks.fetch(TaskFilter::All)?;
397 ///
398 /// // Get today's tasks
399 /// let today = tasks.fetch(TaskFilter::Date(Local::now().date_naive()))?;
400 ///
401 /// // Get tasks tagged as "urgent"
402 /// let urgent = tasks.fetch(TaskFilter::ByTag("urgent".to_string()))?;
403 ///
404 /// // Get incomplete tasks
405 /// let incomplete = tasks.fetch(TaskFilter::Incomplete)?;
406 /// # Ok(())
407 /// # }
408 /// ```
409 ///
410 /// # Performance Notes
411 ///
412 /// - Uses prepared statements for optimal query performance
413 /// - Complex filters like Incomplete may be slower on large datasets
414 /// - Tag information is loaded separately and may add overhead
415 /// - Results are loaded entirely into memory
416 pub fn fetch(&mut self, filter: TaskFilter) -> Result<Vec<Task>> {
417 // Construct the appropriate query and parameters based on filter type
418 let (mut stmt, params): (Statement, Vec<Box<dyn ToSql>>) = match filter {
419 TaskFilter::All => (self.conn.prepare(SELECT_TASKS)?, vec![]),
420 TaskFilter::Date(date) => (self.conn.prepare(&format!("{} {}", SELECT_TASKS, WHERE_DATE))?, vec![Box::new(date)]),
421 TaskFilter::Incomplete => (self.conn.prepare(&format!("{} {}", SELECT_TASKS, WHERE_INCOMPLETE))?, vec![]),
422 TaskFilter::ByIds(ids) => {
423 let ids_params: Vec<Box<dyn ToSql>> = ids.clone().into_iter().map(|id| Box::new(id) as Box<dyn ToSql>).collect();
424 (self.conn.prepare(&Self::query_by_ids(&ids))?, ids_params)
425 }
426 TaskFilter::ByTag(tag_name) => (self.conn.prepare(&format!("{} {}", SELECT_TASKS, WHERE_TAG))?, vec![Box::new(tag_name)]),
427 TaskFilter::ByTags(tag_names) => {
428 let placeholders = vec!["?"; tag_names.len()].join(", ");
429 let query = format!("{} {} ({}))", SELECT_TASKS, WHERE_TAGS, placeholders);
430 let params: Vec<Box<dyn ToSql>> = tag_names.into_iter().map(|name| Box::new(name) as Box<dyn ToSql>).collect();
431 (self.conn.prepare(&query)?, params)
432 }
433 };
434
435 // Execute the query with proper parameter binding
436 let params_refs: Vec<&dyn ToSql> = params.iter().map(|p| &**p).collect();
437 let task_iter = stmt.query_map(¶ms_refs[..], |row| {
438 Ok(Task {
439 id: row.get(0)?,
440 task_id: row.get(1)?,
441 timestamp: row.get(2)?,
442 name: row.get(3)?,
443 comment: row.get(4)?,
444 completeness: row.get(5)?,
445 excluded_from_search: row.get(6)?,
446 tags: vec![], // Tags will be populated in the next step
447 })
448 })?;
449
450 // Collect all task results
451 let mut tasks = Vec::new();
452 for task_result in task_iter {
453 tasks.push(task_result?);
454 }
455
456 // Enrich tasks with tag information
457 let mut tags_db = crate::db::tags::Tags::new()?;
458 for task in &mut tasks {
459 if let Some(task_id) = task.id {
460 task.tags = tags_db.get_tags_by_task(task_id)?;
461 }
462 }
463
464 Ok(tasks)
465 }
466
467 /// Constructs a dynamic SQL query for ID-based task filtering.
468 ///
469 /// This helper method generates the appropriate WHERE clause for filtering
470 /// tasks by a list of specific IDs. It dynamically creates the correct number
471 /// of parameter placeholders to match the provided ID list.
472 ///
473 /// # Arguments
474 ///
475 /// * `ids` - Vector of task IDs to include in the query
476 ///
477 /// # Returns
478 ///
479 /// Returns a properly formatted SQL query string with parameter placeholders.
480 ///
481 /// # Example Output
482 ///
483 /// For `ids = vec![1, 2, 3]`:
484 /// ```sql
485 /// SELECT * FROM tasks WHERE task_id IN (?, ?, ?)
486 /// ```
487 fn query_by_ids(ids: &[i32]) -> String {
488 format!("{} {} ({})", SELECT_TASKS, WHERE_ID_IN, vec!["?"; ids.len()].join(", "))
489 }
490
491 /// Deletes a single task by its unique identifier.
492 ///
493 /// Permanently removes a task record from the database along with all
494 /// associated relationships such as tag assignments. The operation is
495 /// atomic and cannot be undone without database backups.
496 ///
497 /// ## Cascade Effects
498 ///
499 /// Due to foreign key constraints, deleting a task will also:
500 /// - Remove all task-tag associations
501 /// - Update any child tasks that reference this task as parent
502 /// - Trigger any associated database cleanup procedures
503 ///
504 /// # Arguments
505 ///
506 /// * `id` - Unique identifier of the task to delete
507 ///
508 /// # Returns
509 ///
510 /// Returns the number of records affected (0 or 1), or an error if
511 /// the database operation fails.
512 ///
513 /// # Example
514 ///
515 /// ```rust,no_run
516 /// # use kasl::db::tasks::Tasks;
517 /// # fn main() -> anyhow::Result<()> {
518 /// let mut tasks = Tasks::new()?;
519 /// let task_id = 1;
520 /// let deleted_count = tasks.delete(task_id)?;
521 /// if deleted_count > 0 {
522 /// println!("Task deleted successfully");
523 /// }
524 /// # Ok(())
525 /// # }
526 /// ```
527 ///
528 /// # Safety Considerations
529 ///
530 /// - Deletion is immediate and permanent
531 /// - No confirmation prompts at this level
532 /// - Callers should implement appropriate confirmation flows
533 /// - Consider soft deletion for recoverable scenarios
534 pub fn delete(&mut self, id: i32) -> Result<usize> {
535 let affected = self.conn.execute(DELETE_TASK, params![id])?;
536 Ok(affected)
537 }
538
539 /// Efficiently deletes multiple tasks in a single database transaction.
540 ///
541 /// Performs bulk deletion of tasks specified by their IDs, providing
542 /// better performance than individual delete operations and ensuring
543 /// atomicity. All specified tasks and their relationships are removed
544 /// in a single transaction.
545 ///
546 /// ## Performance Benefits
547 ///
548 /// - Single SQL statement execution reduces database round trips
549 /// - Transaction-based operation ensures consistency
550 /// - More efficient than individual deletion loops
551 /// - Reduced lock contention on high-concurrency scenarios
552 ///
553 /// ## Transaction Behavior
554 ///
555 /// The operation is atomic - either all specified tasks are deleted
556 /// or none are deleted if any error occurs. This prevents partial
557 /// deletion states that could lead to data inconsistency.
558 ///
559 /// # Arguments
560 ///
561 /// * `ids` - Slice of task IDs to delete
562 ///
563 /// # Returns
564 ///
565 /// Returns the total number of tasks actually deleted, or an error
566 /// if the database operation fails. Count may be less than input
567 /// length if some IDs don't exist.
568 ///
569 /// # Example
570 ///
571 /// ```rust,no_run
572 /// # use kasl::db::tasks::Tasks;
573 /// # fn main() -> anyhow::Result<()> {
574 /// let mut tasks = Tasks::new()?;
575 /// let ids_to_delete = vec![101, 102, 103];
576 /// let deleted_count = tasks.delete_many(&ids_to_delete)?;
577 /// println!("Deleted {} tasks", deleted_count);
578 /// # Ok(())
579 /// # }
580 /// ```
581 ///
582 /// # Edge Cases
583 ///
584 /// - Empty input slice returns 0 without database interaction
585 /// - Non-existent IDs are silently ignored in the count
586 /// - Very large ID lists may hit database query limits
587 pub fn delete_many(&mut self, ids: &[i32]) -> Result<usize> {
588 // Handle empty input to avoid unnecessary database operations
589 if ids.is_empty() {
590 return Ok(0);
591 }
592
593 // Construct dynamic query with appropriate number of placeholders
594 let placeholders = vec!["?"; ids.len()].join(", ");
595 let query = format!("{} ({})", DELETE_TASKS_BY_IDS, placeholders);
596
597 // Convert IDs to boxed ToSql trait objects for parameter binding
598 let params: Vec<Box<dyn ToSql>> = ids.iter().map(|id| Box::new(*id) as Box<dyn ToSql>).collect();
599 let params_refs: Vec<&dyn ToSql> = params.iter().map(|p| &**p).collect();
600
601 // Execute the bulk deletion query
602 let affected = self.conn.execute(&query, ¶ms_refs[..])?;
603 Ok(affected)
604 }
605
606 /// Checks if a task with the specified ID exists in the database.
607 ///
608 /// Efficiently determines task existence without retrieving the full
609 /// task data. This method is useful for validation before performing
610 /// operations that require existing tasks.
611 ///
612 /// # Arguments
613 ///
614 /// * `id` - Unique identifier of the task to check
615 ///
616 /// # Returns
617 ///
618 /// Returns `true` if the task exists, `false` otherwise, or an error
619 /// if the database query fails.
620 ///
621 /// # Example
622 ///
623 /// ```rust,no_run
624 /// # use kasl::db::tasks::Tasks;
625 /// # fn main() -> anyhow::Result<()> {
626 /// let mut tasks = Tasks::new()?;
627 /// let task_id = 1;
628 /// if tasks.exists(task_id)? {
629 /// println!("Task exists and can be updated");
630 /// } else {
631 /// println!("Task not found");
632 /// }
633 /// # Ok(())
634 /// # }
635 /// ```
636 ///
637 /// # Performance
638 ///
639 /// This method uses COUNT(*) which is optimized for existence checking
640 /// and is more efficient than retrieving full task records when only
641 /// existence verification is needed.
642 pub fn exists(&mut self, id: i32) -> Result<bool> {
643 let count: i32 = self.conn.query_row(SELECT_COUNT_BY_ID, params![id], |row| row.get(0))?;
644 Ok(count > 0)
645 }
646
647 /// Updates an existing task's properties in the database.
648 ///
649 /// Modifies the core properties of an existing task (name, comment, completion)
650 /// while preserving the task's ID, timestamp, and relationships. The task must
651 /// have a valid ID from a previous database operation.
652 ///
653 /// ## Update Scope
654 ///
655 /// This method updates the following fields:
656 /// - **name**: Task title/description
657 /// - **comment**: Detailed description or notes
658 /// - **completeness**: Progress percentage (0-100)
659 ///
660 /// The following fields are **not** modified:
661 /// - **id**: Primary key remains unchanged
662 /// - **timestamp**: Creation time is preserved
663 /// - **task_id**: Parent relationships require separate operations
664 /// - **excluded_from_search**: Search visibility requires separate handling
665 ///
666 /// # Arguments
667 ///
668 /// * `task` - Task object with updated properties and valid ID
669 ///
670 /// # Returns
671 ///
672 /// Returns `Ok(())` if the update succeeds, or an error if the operation
673 /// fails or the task doesn't exist.
674 ///
675 /// # Example
676 ///
677 /// ```rust,no_run
678 /// # use kasl::db::tasks::Tasks;
679 /// # fn main() -> anyhow::Result<()> {
680 /// let mut tasks = Tasks::new()?;
681 /// let task_id = 1;
682 /// let mut task = tasks.get_by_id(task_id)?.unwrap();
683 /// task.name = "Updated task name".to_string();
684 /// task.completeness = Some(75);
685 /// tasks.update(&task)?;
686 /// # Ok(())
687 /// # }
688 /// ```
689 ///
690 /// # Errors
691 ///
692 /// Returns an error if:
693 /// - Task doesn't have a valid ID (not saved to database)
694 /// - No task exists with the specified ID
695 /// - Database constraints are violated
696 /// - Connection or transaction failures occur
697 pub fn update(&mut self, task: &Task) -> Result<()> {
698 // Ensure task has a valid database ID
699 let id = task.id.ok_or_else(|| msg_error_anyhow!(Message::NoIdSet))?;
700
701 // Execute update query and check if any rows were affected
702 let affected = self.conn.execute(UPDATE_TASK, params![task.name, task.comment, task.completeness, id])?;
703
704 // Verify that the task actually existed and was updated
705 if affected == 0 {
706 return Err(msg_error_anyhow!(Message::TaskUpdateFailed));
707 }
708
709 Ok(())
710 }
711
712 /// Retrieves a single task by its unique identifier.
713 ///
714 /// This convenience method fetches a complete task record including
715 /// all associated tags and properties. It's a specialized version of
716 /// the fetch method optimized for single-task retrieval.
717 ///
718 /// # Arguments
719 ///
720 /// * `id` - Unique identifier of the task to retrieve
721 ///
722 /// # Returns
723 ///
724 /// Returns `Some(Task)` if found, `None` if the task doesn't exist,
725 /// or an error if the database query fails.
726 ///
727 /// # Example
728 ///
729 /// ```rust,no_run
730 /// # use kasl::db::tasks::Tasks;
731 /// # fn main() -> anyhow::Result<()> {
732 /// let mut tasks = Tasks::new()?;
733 /// if let Some(task) = tasks.get_by_id(42)? {
734 /// println!("Found task: {}", task.name);
735 /// } else {
736 /// println!("Task with ID 42 not found");
737 /// }
738 /// # Ok(())
739 /// # }
740 /// ```
741 ///
742 /// # Performance
743 ///
744 /// This method internally uses the fetch mechanism with ID filtering,
745 /// so it includes full tag loading and relationship resolution.
746 /// For simple existence checking, use the `exists` method instead.
747 pub fn get_by_id(&mut self, id: i32) -> Result<Option<Task>> {
748 let mut tasks = self.fetch(TaskFilter::ByIds(vec![id]))?;
749 Ok(tasks.pop())
750 }
751}