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