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,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(&params_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, &params_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}