Skip to main content

kasl/db/
tags.rs

1//! Tag-based task categorization and organization system.
2//!
3//! Provides functionality for managing tags that can be associated with tasks
4//! for categorization, filtering, and organization purposes. Supports many-to-many
5//! relationships between tasks and tags.
6//!
7//! ## Features
8//!
9//! - **Tag Management**: Create, update, delete, and query tag definitions
10//! - **Color Coding**: Optional color assignment for visual task organization
11//! - **Task Association**: Link tags to tasks with automatic relationship management
12//! - **Bulk Operations**: Efficient creation and association of multiple tags
13//! - **Search & Filter**: Find tags by name and retrieve tasks by tag association
14//!
15//! ## Usage
16//!
17//! ```rust
18//! use kasl::db::tags::{Tags, Tag};
19//!
20//! let mut tags = Tags::new()?;
21//! let urgent_tag = Tag::new("urgent".to_string(), Some("red".to_string()));
22//! let tag_id = tags.create(&urgent_tag)?;
23//! tags.add_tag_to_task(task_id, tag_id)?;
24//! ```
25
26use crate::db::db::Db;
27use crate::libs::messages::Message;
28use crate::msg_error_anyhow;
29use anyhow::Result;
30use rusqlite::{params, Connection, OptionalExtension};
31use serde::{Deserialize, Serialize};
32
33/// SQL schema for the main tags table.
34///
35/// Stores tag definitions with unique names and optional color codes.
36/// The table supports efficient lookups by name and provides creation
37/// timestamps for audit trails.
38const SCHEMA_TAGS: &str = "CREATE TABLE IF NOT EXISTS tags (
39    id INTEGER PRIMARY KEY,
40    name TEXT NOT NULL UNIQUE,
41    color TEXT,
42    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
43)";
44
45/// SQL schema for the task-tag relationship junction table.
46///
47/// Implements a many-to-many relationship between tasks and tags using
48/// foreign key constraints and composite primary keys. Cascade deletions
49/// ensure referential integrity when tasks or tags are removed.
50const SCHEMA_TASK_TAGS: &str = "CREATE TABLE IF NOT EXISTS task_tags (
51    task_id INTEGER NOT NULL,
52    tag_id INTEGER NOT NULL,
53    PRIMARY KEY (task_id, tag_id),
54    FOREIGN KEY (task_id) REFERENCES tasks(id) ON DELETE CASCADE,
55    FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
56)";
57
58/// Insert a new tag record with name and optional color.
59///
60/// Creates a new tag definition in the database with automatic ID assignment
61/// and timestamp generation. Tag names must be unique across the system.
62const INSERT_TAG: &str = "INSERT INTO tags (name, color) VALUES (?1, ?2)";
63
64/// Update an existing tag's name and color properties.
65///
66/// Modifies tag properties while preserving the original creation timestamp
67/// and maintaining referential integrity with existing task associations.
68const UPDATE_TAG: &str = "UPDATE tags SET name = ?2, color = ?3 WHERE id = ?1";
69
70/// Delete a tag record and all associated task relationships.
71///
72/// Removes the tag definition and automatically cleans up all task-tag
73/// associations through foreign key cascade constraints.
74const DELETE_TAG: &str = "DELETE FROM tags WHERE id = ?1";
75
76/// Retrieve all tags ordered alphabetically by name.
77///
78/// Provides a complete list of tag definitions sorted for consistent
79/// display in user interfaces and reports.
80const SELECT_ALL_TAGS: &str = "SELECT * FROM tags ORDER BY name";
81
82/// Find a specific tag by its unique name.
83///
84/// Enables case-sensitive tag lookup for validation and duplicate
85/// prevention during tag creation and management.
86const SELECT_TAG_BY_NAME: &str = "SELECT * FROM tags WHERE name = ?1";
87
88/// Retrieve a tag record by its unique identifier.
89///
90/// Provides direct access to tag details using the primary key for
91/// efficient lookups during task-tag association operations.
92const SELECT_TAG_BY_ID: &str = "SELECT * FROM tags WHERE id = ?1";
93
94/// Get all tags associated with a specific task.
95///
96/// Joins the tags and task_tags tables to retrieve complete tag information
97/// for a given task, ordered alphabetically for consistent display.
98const SELECT_TAGS_BY_TASK: &str = "
99    SELECT t.* FROM tags t
100    JOIN task_tags tt ON t.id = tt.tag_id
101    WHERE tt.task_id = ?1
102    ORDER BY t.name
103";
104
105/// Find all tasks associated with a specific tag.
106///
107/// Retrieves task IDs that are associated with the given tag identifier,
108/// useful for tag-based task filtering and reporting.
109const SELECT_TASKS_BY_TAG: &str = "SELECT task_id FROM task_tags WHERE tag_id = ?1";
110
111/// Create a new task-tag association.
112///
113/// Links a task with a tag using the junction table. The OR IGNORE clause
114/// prevents errors when the association already exists.
115const INSERT_TASK_TAG: &str = "INSERT OR IGNORE INTO task_tags (task_id, tag_id) VALUES (?1, ?2)";
116
117/// Remove a specific task-tag association.
118///
119/// Unlinks a tag from a task without affecting other relationships or
120/// the tag/task definitions themselves.
121const DELETE_TASK_TAG: &str = "DELETE FROM task_tags WHERE task_id = ?1 AND tag_id = ?2";
122
123/// Remove all tag associations for a specific task.
124///
125/// Clears all tags from a task, typically used when deleting tasks or
126/// when users want to reset a task's tag assignments.
127const DELETE_ALL_TASK_TAGS: &str = "DELETE FROM task_tags WHERE task_id = ?1";
128
129/// Represents a tag entity with its properties and metadata.
130///
131/// A tag is a label that can be associated with tasks for categorization
132/// and organization. Tags support optional color coding for visual
133/// organization in user interfaces.
134///
135/// ## Field Details
136///
137/// - **id**: Database-assigned unique identifier (None for new tags)
138/// - **name**: Human-readable tag name (must be unique)
139/// - **color**: Optional color code for visual categorization
140/// - **created_at**: Timestamp of tag creation (managed by database)
141///
142/// ## Serialization
143///
144/// The struct supports JSON serialization for data export and API
145/// responses, making it suitable for configuration files and web interfaces.
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct Tag {
148    /// Unique identifier assigned by the database.
149    ///
150    /// This field is `None` for new tags that haven't been saved to the
151    /// database yet, and `Some(id)` for existing tags retrieved from storage.
152    pub id: Option<i32>,
153
154    /// Unique name identifier for the tag.
155    ///
156    /// Tag names must be unique across the system and are used for
157    /// human-readable identification. Names are case-sensitive and
158    /// should follow consistent naming conventions.
159    pub name: String,
160
161    /// Optional color code for visual categorization.
162    ///
163    /// Colors can be specified as hex codes, CSS color names, or any
164    /// format supported by the user interface. This field enables
165    /// visual organization and quick recognition of tag categories.
166    pub color: Option<String>,
167
168    /// Timestamp when the tag was created.
169    ///
170    /// Automatically managed by the database and used for audit trails
171    /// and chronological sorting. Format depends on database settings.
172    pub created_at: Option<String>,
173}
174
175impl Tag {
176    /// Creates a new tag instance with the specified name and optional color.
177    ///
178    /// This constructor creates a tag object ready for database insertion.
179    /// The ID and creation timestamp are set by the database when the tag
180    /// is saved using the `Tags::create()` method.
181    ///
182    /// # Arguments
183    ///
184    /// * `name` - Unique name for the tag (will be validated for uniqueness)
185    /// * `color` - Optional color code for visual organization
186    ///
187    /// # Returns
188    ///
189    /// Returns a new `Tag` instance ready for database operations.
190    ///
191    /// # Example
192    ///
193    /// ```rust
194    /// use kasl::db::tags::Tag;
195    ///
196    /// // Create a tag with color
197    /// let urgent_tag = Tag::new("urgent".to_string(), Some("red".to_string()));
198    ///
199    /// // Create a tag without color
200    /// let general_tag = Tag::new("general".to_string(), None);
201    /// ```
202    pub fn new(name: String, color: Option<String>) -> Self {
203        Self {
204            id: None,
205            name,
206            color,
207            created_at: None,
208        }
209    }
210}
211
212/// Database manager for tag operations and task-tag relationships.
213///
214/// The `Tags` struct provides a high-level interface for managing tags and
215/// their associations with tasks. It handles database connections, schema
216/// initialization, and provides methods for all tag-related operations.
217///
218/// ## Functionality
219///
220/// - **CRUD Operations**: Create, read, update, and delete tag definitions
221/// - **Relationship Management**: Associate and disassociate tags with tasks
222/// - **Batch Operations**: Efficiently handle multiple tag operations
223/// - **Query Support**: Search and filter tags and their associations
224///
225/// ## Database Schema Management
226///
227/// The struct automatically ensures that required database tables exist
228/// when instantiated, handling schema creation and migration compatibility.
229pub struct Tags {
230    /// Database connection for tag operations.
231    ///
232    /// Direct connection to the SQLite database for executing tag-related
233    /// queries. The connection is managed internally and provides transactional
234    /// support for complex operations.
235    conn: Connection,
236}
237
238impl Tags {
239    /// Creates a new Tags manager and initializes the database schema.
240    ///
241    /// This constructor establishes a database connection, ensures that the
242    /// required tables exist, and prepares the manager for tag operations.
243    /// Schema creation is idempotent and safe to call multiple times.
244    ///
245    /// # Returns
246    ///
247    /// Returns a new `Tags` instance ready for tag management operations,
248    /// or an error if database initialization fails.
249    ///
250    /// # Example
251    ///
252    /// ```rust
253    /// use kasl::db::tags::Tags;
254    ///
255    /// let mut tags = Tags::new()?;
256    /// // Ready for tag operations
257    /// ```
258    ///
259    /// # Errors
260    ///
261    /// Returns an error if:
262    /// - Database connection cannot be established
263    /// - Schema creation fails due to permissions or corruption
264    /// - Migration system encounters errors during table setup
265    pub fn new() -> Result<Self> {
266        let db = Db::new()?;
267
268        // Initialize tag system tables (migration v3 creates them, but ensure they exist)
269        db.conn.execute(SCHEMA_TAGS, [])?;
270        db.conn.execute(SCHEMA_TASK_TAGS, [])?;
271
272        Ok(Tags { conn: db.conn })
273    }
274
275    /// Creates a new tag in the database and returns its assigned ID.
276    ///
277    /// This method inserts a new tag record with the provided name and color,
278    /// automatically assigning a unique ID and creation timestamp. Tag names
279    /// must be unique across the system.
280    ///
281    /// # Arguments
282    ///
283    /// * `tag` - Tag object containing name and optional color
284    ///
285    /// # Returns
286    ///
287    /// Returns the database-assigned ID for the new tag, or an error if
288    /// creation fails (e.g., due to duplicate names).
289    ///
290    /// # Example
291    ///
292    /// ```rust
293    /// use kasl::db::tags::{Tags, Tag};
294    ///
295    /// let mut tags = Tags::new()?;
296    /// let tag = Tag::new("priority".to_string(), Some("orange".to_string()));
297    /// let tag_id = tags.create(&tag)?;
298    /// println!("Created tag with ID: {}", tag_id);
299    /// ```
300    ///
301    /// # Errors
302    ///
303    /// Returns an error if:
304    /// - A tag with the same name already exists
305    /// - Database constraints are violated
306    /// - Connection or transaction failures occur
307    pub fn create(&mut self, tag: &Tag) -> Result<i32> {
308        self.conn.execute(INSERT_TAG, params![tag.name, tag.color])?;
309        Ok(self.conn.last_insert_rowid() as i32)
310    }
311
312    /// Updates an existing tag's name and color properties.
313    ///
314    /// This method modifies an existing tag's properties while preserving
315    /// its ID, creation timestamp, and task associations. The updated name
316    /// must still be unique across all tags.
317    ///
318    /// # Arguments
319    ///
320    /// * `tag` - Tag object with updated properties (must have a valid ID)
321    ///
322    /// # Returns
323    ///
324    /// Returns `Ok(())` if the update succeeds, or an error if the operation
325    /// fails or the tag doesn't exist.
326    ///
327    /// # Example
328    ///
329    /// ```rust
330    /// let mut tags = Tags::new()?;
331    /// let mut tag = tags.get_by_id(tag_id)?.unwrap();
332    /// tag.name = "high-priority".to_string();
333    /// tag.color = Some("crimson".to_string());
334    /// tags.update(&tag)?;
335    /// ```
336    ///
337    /// # Errors
338    ///
339    /// Returns an error if:
340    /// - The tag ID doesn't exist in the database
341    /// - The new name conflicts with an existing tag
342    /// - Database constraints are violated
343    pub fn update(&mut self, tag: &Tag) -> Result<()> {
344        let id = tag.id.ok_or_else(|| msg_error_anyhow!(Message::TagNotFound(tag.name.to_string())))?;
345
346        let affected = self.conn.execute(UPDATE_TAG, params![id, tag.name, tag.color])?;
347
348        if affected == 0 {
349            return Err(msg_error_anyhow!(Message::TagNotFound(tag.name.to_string())));
350        }
351
352        Ok(())
353    }
354
355    /// Deletes a tag and all its task associations from the database.
356    ///
357    /// This method permanently removes a tag definition and automatically
358    /// cleans up all task-tag relationships through foreign key cascading.
359    /// The operation is atomic and cannot be undone without database backups.
360    ///
361    /// # Arguments
362    ///
363    /// * `id` - Unique identifier of the tag to delete
364    ///
365    /// # Returns
366    ///
367    /// Returns `Ok(())` if deletion succeeds, or an error if the operation
368    /// fails. Deleting a non-existent tag is not considered an error.
369    ///
370    /// # Example
371    ///
372    /// ```rust
373    /// let mut tags = Tags::new()?;
374    /// tags.delete(tag_id)?; // Removes tag and all associations
375    /// ```
376    ///
377    /// # Side Effects
378    ///
379    /// - Removes the tag definition from the tags table
380    /// - Automatically removes all task-tag associations via CASCADE
381    /// - Cannot be undone without database restore operations
382    pub fn delete(&mut self, id: i32) -> Result<()> {
383        let affected = self.conn.execute(DELETE_TAG, params![id])?;
384        if affected == 0 {
385            return Err(msg_error_anyhow!(Message::TagNotFound(id.to_string())));
386        }
387        Ok(())
388    }
389
390    /// Retrieves all tags from the database ordered alphabetically.
391    ///
392    /// This method returns a complete list of all tag definitions sorted
393    /// by name for consistent display in user interfaces and reports.
394    /// The list includes all tag properties including colors and timestamps.
395    ///
396    /// # Returns
397    ///
398    /// Returns a vector of all tag records ordered by name, or an error
399    /// if the database query fails.
400    ///
401    /// # Example
402    ///
403    /// ```rust
404    /// let mut tags = Tags::new()?;
405    /// let all_tags = tags.get_all()?;
406    /// for tag in all_tags {
407    ///     println!("Tag: {} ({})", tag.name, tag.color.unwrap_or("no color".to_string()));
408    /// }
409    /// ```
410    ///
411    /// # Performance Considerations
412    ///
413    /// This method loads all tags into memory, which is efficient for small
414    /// to medium tag collections but may need pagination for very large datasets.
415    pub fn get_all(&mut self) -> Result<Vec<Tag>> {
416        let mut stmt = self.conn.prepare(SELECT_ALL_TAGS)?;
417        let tag_iter = stmt.query_map([], |row| {
418            Ok(Tag {
419                id: row.get(0)?,
420                name: row.get(1)?,
421                color: row.get(2)?,
422                created_at: row.get(3)?,
423            })
424        })?;
425
426        let mut tags = Vec::new();
427        for tag in tag_iter {
428            tags.push(tag?);
429        }
430        Ok(tags)
431    }
432
433    /// Finds a tag by its unique name with case-sensitive matching.
434    ///
435    /// This method performs an exact name lookup to find a specific tag
436    /// definition. It's commonly used for validation during tag creation
437    /// and for resolving tag names to IDs in user commands.
438    ///
439    /// # Arguments
440    ///
441    /// * `name` - Exact name of the tag to find (case-sensitive)
442    ///
443    /// # Returns
444    ///
445    /// Returns `Some(Tag)` if found, `None` if no matching tag exists,
446    /// or an error if the database query fails.
447    ///
448    /// # Example
449    ///
450    /// ```rust
451    /// let mut tags = Tags::new()?;
452    /// if let Some(tag) = tags.get_by_name("urgent")? {
453    ///     println!("Found tag: {} with color: {:?}", tag.name, tag.color);
454    /// } else {
455    ///     println!("Tag 'urgent' not found");
456    /// }
457    /// ```
458    ///
459    /// # Name Matching
460    ///
461    /// - Performs exact, case-sensitive string matching
462    /// - Does not support wildcards or partial matching
463    /// - Whitespace is significant and must match exactly
464    pub fn get_by_name(&mut self, name: &str) -> Result<Option<Tag>> {
465        let tag = self
466            .conn
467            .query_row(SELECT_TAG_BY_NAME, params![name], |row| {
468                Ok(Tag {
469                    id: row.get(0)?,
470                    name: row.get(1)?,
471                    color: row.get(2)?,
472                    created_at: row.get(3)?,
473                })
474            })
475            .optional()?;
476        Ok(tag)
477    }
478
479    /// Retrieves a tag by its unique database identifier.
480    ///
481    /// This method provides direct access to tag details using the primary
482    /// key for efficient lookups during task-tag association operations
483    /// and when processing user commands with tag IDs.
484    ///
485    /// # Arguments
486    ///
487    /// * `id` - Unique database identifier of the tag
488    ///
489    /// # Returns
490    ///
491    /// Returns `Some(Tag)` if found, `None` if the ID doesn't exist,
492    /// or an error if the database query fails.
493    ///
494    /// # Example
495    ///
496    /// ```rust
497    /// let mut tags = Tags::new()?;
498    /// if let Some(tag) = tags.get_by_id(42)? {
499    ///     println!("Tag ID 42: {}", tag.name);
500    /// }
501    /// ```
502    ///
503    /// # Performance
504    ///
505    /// This is the most efficient way to retrieve a tag when the ID is known,
506    /// as it uses the primary key index for direct record access.
507    pub fn get_by_id(&mut self, id: i32) -> Result<Option<Tag>> {
508        let tag = self
509            .conn
510            .query_row(SELECT_TAG_BY_ID, params![id], |row| {
511                Ok(Tag {
512                    id: row.get(0)?,
513                    name: row.get(1)?,
514                    color: row.get(2)?,
515                    created_at: row.get(3)?,
516                })
517            })
518            .optional()?;
519        Ok(tag)
520    }
521
522    /// Retrieves all tags associated with a specific task.
523    ///
524    /// This method returns the complete set of tags linked to a task,
525    /// ordered alphabetically for consistent display. It joins the tags
526    /// and task_tags tables to provide full tag information.
527    ///
528    /// # Arguments
529    ///
530    /// * `task_id` - Unique identifier of the task
531    ///
532    /// # Returns
533    ///
534    /// Returns a vector of tags associated with the task, or an error
535    /// if the database query fails.
536    ///
537    /// # Example
538    ///
539    /// ```rust
540    /// let mut tags = Tags::new()?;
541    /// let task_tags = tags.get_tags_by_task(task_id)?;
542    /// for tag in task_tags {
543    ///     println!("Task has tag: {}", tag.name);
544    /// }
545    /// ```
546    ///
547    /// # Return Characteristics
548    ///
549    /// - Empty vector if the task has no tags
550    /// - Results ordered alphabetically by tag name
551    /// - Includes full tag details (name, color, timestamps)
552    pub fn get_tags_by_task(&mut self, task_id: i32) -> Result<Vec<Tag>> {
553        let mut stmt = self.conn.prepare(SELECT_TAGS_BY_TASK)?;
554        let tag_iter = stmt.query_map(params![task_id], |row| {
555            Ok(Tag {
556                id: row.get(0)?,
557                name: row.get(1)?,
558                color: row.get(2)?,
559                created_at: row.get(3)?,
560            })
561        })?;
562
563        let mut tags = Vec::new();
564        for tag in tag_iter {
565            tags.push(tag?);
566        }
567        Ok(tags)
568    }
569
570    /// Finds all tasks associated with a specific tag.
571    ///
572    /// This method returns the list of task IDs that have been tagged
573    /// with the specified tag. It's useful for tag-based filtering and
574    /// generating reports of tasks by category.
575    ///
576    /// # Arguments
577    ///
578    /// * `tag_id` - Unique identifier of the tag
579    ///
580    /// # Returns
581    ///
582    /// Returns a vector of task IDs associated with the tag, or an error
583    /// if the database query fails.
584    ///
585    /// # Example
586    ///
587    /// ```rust
588    /// let mut tags = Tags::new()?;
589    /// let task_ids = tags.get_tasks_by_tag(tag_id)?;
590    /// println!("Tag is used by {} tasks", task_ids.len());
591    /// ```
592    ///
593    /// # Use Cases
594    ///
595    /// - Tag-based task filtering in reports
596    /// - Counting tag usage frequency
597    /// - Validating tag deletion impact
598    /// - Generating tag-specific task lists
599    pub fn get_tasks_by_tag(&mut self, tag_id: i32) -> Result<Vec<i32>> {
600        let mut stmt = self.conn.prepare(SELECT_TASKS_BY_TAG)?;
601        let task_iter = stmt.query_map(params![tag_id], |row| row.get(0))?;
602
603        let mut task_ids = Vec::new();
604        for task_id in task_iter {
605            task_ids.push(task_id?);
606        }
607        Ok(task_ids)
608    }
609
610    /// Associates a tag with a task, creating a many-to-many relationship.
611    ///
612    /// This method creates a link between a task and a tag in the junction
613    /// table. If the association already exists, the operation succeeds
614    /// without creating duplicates due to the OR IGNORE clause.
615    ///
616    /// # Arguments
617    ///
618    /// * `task_id` - Unique identifier of the task
619    /// * `tag_id` - Unique identifier of the tag
620    ///
621    /// # Returns
622    ///
623    /// Returns `Ok(())` if the association is created or already exists,
624    /// or an error if the database operation fails.
625    ///
626    /// # Example
627    ///
628    /// ```rust
629    /// let mut tags = Tags::new()?;
630    /// tags.add_tag_to_task(task_id, tag_id)?;
631    /// println!("Tag associated with task");
632    /// ```
633    ///
634    /// # Idempotency
635    ///
636    /// This operation is idempotent - calling it multiple times with the
637    /// same parameters has the same effect as calling it once.
638    pub fn add_tag_to_task(&mut self, task_id: i32, tag_id: i32) -> Result<()> {
639        self.conn.execute(INSERT_TASK_TAG, params![task_id, tag_id])?;
640        Ok(())
641    }
642
643    /// Removes a specific tag association from a task.
644    ///
645    /// This method removes the link between a task and a tag without
646    /// affecting the tag definition or other task associations. The
647    /// operation is safe and succeeds even if the association doesn't exist.
648    ///
649    /// # Arguments
650    ///
651    /// * `task_id` - Unique identifier of the task
652    /// * `tag_id` - Unique identifier of the tag to remove
653    ///
654    /// # Returns
655    ///
656    /// Returns `Ok(())` if the removal succeeds, or an error if the
657    /// database operation fails.
658    ///
659    /// # Example
660    ///
661    /// ```rust
662    /// let mut tags = Tags::new()?;
663    /// tags.remove_tag_from_task(task_id, tag_id)?;
664    /// println!("Tag removed from task");
665    /// ```
666    ///
667    /// # Side Effects
668    ///
669    /// - Only affects the specific task-tag relationship
670    /// - Does not delete the tag or task definitions
671    /// - Safe to call even if the association doesn't exist
672    pub fn remove_tag_from_task(&mut self, task_id: i32, tag_id: i32) -> Result<()> {
673        self.conn.execute(DELETE_TASK_TAG, params![task_id, tag_id])?;
674        Ok(())
675    }
676
677    /// Removes all tag associations from a specific task.
678    ///
679    /// This method clears all tags from a task, effectively resetting its
680    /// tag assignments. It's commonly used when deleting tasks or when
681    /// users want to completely re-tag a task.
682    ///
683    /// # Arguments
684    ///
685    /// * `task_id` - Unique identifier of the task to clear
686    ///
687    /// # Returns
688    ///
689    /// Returns the number of associations removed, or an error if the
690    /// database operation fails.
691    ///
692    /// # Example
693    ///
694    /// ```rust
695    /// let mut tags = Tags::new()?;
696    /// let removed_count = tags.remove_all_tags_from_task(task_id)?;
697    /// println!("Removed {} tag associations", removed_count);
698    /// ```
699    ///
700    /// # Use Cases
701    ///
702    /// - Task deletion cleanup
703    /// - Bulk tag reassignment workflows
704    /// - Resetting task categorization
705    /// - Data migration and correction operations
706    pub fn remove_all_tags_from_task(&mut self, task_id: i32) -> Result<usize> {
707        let affected = self.conn.execute(DELETE_ALL_TASK_TAGS, params![task_id])?;
708        Ok(affected)
709    }
710
711    /// Replaces all tag associations for a task with a new set of tags.
712    ///
713    /// This method provides atomic tag assignment by completely replacing
714    /// a task's current tag associations with a new set. It ensures that
715    /// the task ends up with exactly the specified tags, regardless of
716    /// its previous tag state.
717    ///
718    /// ## Operation Sequence
719    ///
720    /// The method performs a two-step atomic operation:
721    /// 1. **Clear Existing**: Removes all current tag associations for the task
722    /// 2. **Add New**: Creates associations for each specified tag ID
723    ///
724    /// This approach ensures consistency and prevents partial update states
725    /// that could occur with manual add/remove operations.
726    ///
727    /// ## Transaction Semantics
728    ///
729    /// While not explicitly wrapped in a transaction, the operation is
730    /// designed to be atomic - either all associations are updated successfully
731    /// or the task retains its original tag state if any error occurs.
732    ///
733    /// ## Use Cases
734    ///
735    /// - **Bulk Tag Assignment**: Assigning multiple tags to a task at once
736    /// - **Tag Replacement**: Completely changing a task's tag categorization
737    /// - **Import Operations**: Setting tags during data import or migration
738    /// - **Template Application**: Applying predefined tag sets from templates
739    /// - **UI Operations**: Saving tag selections from multi-select interfaces
740    ///
741    /// # Arguments
742    ///
743    /// * `task_id` - Unique identifier of the task to update
744    /// * `tag_ids` - Slice of tag IDs to associate with the task
745    ///
746    /// # Returns
747    ///
748    /// Returns `Ok(())` if all tag associations are updated successfully,
749    /// or an error if any database operation fails.
750    ///
751    /// # Example
752    ///
753    /// ```rust
754    /// let mut tags = Tags::new()?;
755    ///
756    /// // Replace task tags with new set
757    /// let new_tag_ids = vec![1, 3, 5]; // urgent, backend, review
758    /// tags.set_task_tags(task_id, &new_tag_ids)?;
759    ///
760    /// // Task now has exactly these three tags
761    /// let current_tags = tags.get_tags_by_task(task_id)?;
762    /// assert_eq!(current_tags.len(), 3);
763    /// ```
764    ///
765    /// # Performance Considerations
766    ///
767    /// - **Efficient for Large Changes**: More efficient than individual add/remove operations
768    /// - **Database Operations**: Minimizes database round-trips through batch processing
769    /// - **Index Usage**: Leverages database indices for both clear and insert operations
770    /// - **Memory Usage**: Tag ID slice is processed iteratively to minimize memory usage
771    ///
772    /// # Error Handling
773    ///
774    /// If the clear operation succeeds but adding new tags fails, the task
775    /// will be left with no tags. Callers should be prepared to handle this
776    /// scenario and potentially retry the operation or restore previous state.
777    ///
778    /// # Data Integrity
779    ///
780    /// The method assumes all provided tag IDs exist in the database. Non-existent
781    /// tag IDs will cause foreign key constraint violations and operation failure.
782    /// Use `get_or_create_tags()` if tag existence is uncertain.
783    pub fn set_task_tags(&mut self, task_id: i32, tag_ids: &[i32]) -> Result<()> {
784        // Clear all existing tag associations for this task
785        self.remove_all_tags_from_task(task_id)?;
786
787        // Add each new tag association
788        for tag_id in tag_ids {
789            self.add_tag_to_task(task_id, *tag_id)?;
790        }
791
792        Ok(())
793    }
794
795    /// Creates or retrieves tags by name, returning their database IDs.
796    ///
797    /// This convenience method handles the common workflow of ensuring tags
798    /// exist before associating them with tasks. For each provided name,
799    /// it either returns the existing tag ID or creates a new tag with
800    /// a default color.
801    ///
802    /// ## Batch Processing
803    ///
804    /// The method processes multiple tag names efficiently, checking for
805    /// existence before creation and returning a complete list of IDs
806    /// ready for task association.
807    ///
808    /// # Arguments
809    ///
810    /// * `names` - Slice of tag names to create or retrieve
811    ///
812    /// # Returns
813    ///
814    /// Returns a vector of tag IDs corresponding to the input names,
815    /// or an error if any database operation fails.
816    ///
817    /// # Example
818    ///
819    /// ```rust
820    /// let mut tags = Tags::new()?;
821    /// let tag_names = vec!["urgent".to_string(), "backend".to_string()];
822    /// let tag_ids = tags.get_or_create_tags(&tag_names)?;
823    /// // tag_ids now contains IDs for both tags (created if needed)
824    /// ```
825    ///
826    /// # Default Color Assignment
827    ///
828    /// New tags are assigned colors from a predefined rotation to ensure
829    /// visual variety. The color assignment is deterministic but varies
830    /// across different tag creation sessions.
831    pub fn get_or_create_tags(&mut self, names: &[String]) -> Result<Vec<i32>> {
832        let mut tag_ids = Vec::new();
833
834        for name in names {
835            let tag = match self.get_by_name(name)? {
836                Some(existing_tag) => existing_tag,
837                None => {
838                    // Create new tag with a default color from rotation
839                    let tag = Tag::new(name.clone(), Some(Self::get_default_color()));
840                    let id = self.create(&tag)?;
841                    Tag {
842                        id: Some(id),
843                        name: name.clone(),
844                        color: tag.color,
845                        created_at: None,
846                    }
847                }
848            };
849
850            if let Some(id) = tag.id {
851                tag_ids.push(id);
852            }
853        }
854
855        Ok(tag_ids)
856    }
857
858    /// Generates a default color for new tags using a rotation algorithm.
859    ///
860    /// This internal method provides automatic color assignment for tags
861    /// created without explicit color specifications. It cycles through
862    /// a predefined set of colors to ensure visual variety and consistency.
863    ///
864    /// ## Color Rotation
865    ///
866    /// The method maintains a static counter that advances through a
867    /// predefined color palette, ensuring that consecutively created
868    /// tags receive different colors for better visual distinction.
869    ///
870    /// # Returns
871    ///
872    /// Returns a color name string from the predefined palette.
873    ///
874    /// # Thread Safety
875    ///
876    /// This method uses unsafe static mutation for simplicity. In a
877    /// multi-threaded environment, this could lead to race conditions,
878    /// but the impact is minimal (just color selection variation).
879    ///
880    /// # Color Palette
881    ///
882    /// The current palette includes: blue, green, yellow, red, purple,
883    /// cyan, and orange, providing good visual variety for most use cases.
884    fn get_default_color() -> String {
885        // Predefined color palette for automatic assignment
886        static COLORS: &[&str] = &["blue", "green", "yellow", "red", "purple", "cyan", "orange"];
887        static mut COLOR_INDEX: usize = 0;
888
889        unsafe {
890            let color = COLORS[COLOR_INDEX % COLORS.len()];
891            COLOR_INDEX += 1;
892            color.to_string()
893        }
894    }
895}