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(¶ms_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, ¶ms_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}