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::{Connection, OptionalExtension, params};
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}