Skip to main content

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