tempo_cli/db/
queries.rs

1use anyhow::Result;
2use rusqlite::{params, Connection, OptionalExtension};
3use crate::models::{Project, Session, Tag};
4use std::path::PathBuf;
5
6pub struct ProjectQueries;
7
8impl ProjectQueries {
9    pub fn create(conn: &Connection, project: &Project) -> Result<i64> {
10        let mut stmt = conn.prepare(
11            "INSERT INTO projects (name, path, git_hash, description, is_archived)
12             VALUES (?1, ?2, ?3, ?4, ?5)"
13        )?;
14        
15        stmt.execute(params![
16            project.name,
17            project.path.to_string_lossy().to_string(),
18            project.git_hash,
19            project.description,
20            project.is_archived
21        ])?;
22        
23        Ok(conn.last_insert_rowid())
24    }
25    
26    pub fn find_by_path(conn: &Connection, path: &PathBuf) -> Result<Option<Project>> {
27        let mut stmt = conn.prepare(
28            "SELECT id, name, path, git_hash, created_at, updated_at, is_archived, description
29             FROM projects WHERE path = ?1"
30        )?;
31        
32        let project = stmt.query_row([path.to_string_lossy().to_string()], |row| {
33            Ok(Project {
34                id: Some(row.get(0)?),
35                name: row.get(1)?,
36                path: PathBuf::from(row.get::<_, String>(2)?),
37                git_hash: row.get(3)?,
38                created_at: row.get(4)?,
39                updated_at: row.get(5)?,
40                is_archived: row.get(6)?,
41                description: row.get(7)?,
42            })
43        }).optional()?;
44        
45        Ok(project)
46    }
47    
48    pub fn list_all(conn: &Connection, include_archived: bool) -> Result<Vec<Project>> {
49        let sql = if include_archived {
50            "SELECT id, name, path, git_hash, created_at, updated_at, is_archived, description
51             FROM projects ORDER BY name"
52        } else {
53            "SELECT id, name, path, git_hash, created_at, updated_at, is_archived, description
54             FROM projects WHERE is_archived = 0 ORDER BY name"
55        };
56        
57        let mut stmt = conn.prepare(sql)?;
58        let projects = stmt.query_map([], |row| {
59            Ok(Project {
60                id: Some(row.get(0)?),
61                name: row.get(1)?,
62                path: PathBuf::from(row.get::<_, String>(2)?),
63                git_hash: row.get(3)?,
64                created_at: row.get(4)?,
65                updated_at: row.get(5)?,
66                is_archived: row.get(6)?,
67                description: row.get(7)?,
68            })
69        })?.collect::<Result<Vec<_>, _>>()?;
70        
71        Ok(projects)
72    }
73    
74    pub fn find_by_id(conn: &Connection, project_id: i64) -> Result<Option<Project>> {
75        let mut stmt = conn.prepare(
76            "SELECT id, name, path, git_hash, created_at, updated_at, is_archived, description
77             FROM projects WHERE id = ?1"
78        )?;
79        
80        let project = stmt.query_row([project_id], |row| {
81            Ok(Project {
82                id: Some(row.get(0)?),
83                name: row.get(1)?,
84                path: PathBuf::from(row.get::<_, String>(2)?),
85                git_hash: row.get(3)?,
86                created_at: row.get(4)?,
87                updated_at: row.get(5)?,
88                is_archived: row.get(6)?,
89                description: row.get(7)?,
90            })
91        }).optional()?;
92        
93        Ok(project)
94    }
95    
96    pub fn find_by_name(conn: &Connection, name: &str) -> Result<Option<Project>> {
97        let mut stmt = conn.prepare(
98            "SELECT id, name, path, git_hash, created_at, updated_at, is_archived, description
99             FROM projects WHERE name = ?1"
100        )?;
101        
102        let project = stmt.query_row([name], |row| {
103            Ok(Project {
104                id: Some(row.get(0)?),
105                name: row.get(1)?,
106                path: PathBuf::from(row.get::<_, String>(2)?),
107                git_hash: row.get(3)?,
108                created_at: row.get(4)?,
109                updated_at: row.get(5)?,
110                is_archived: row.get(6)?,
111                description: row.get(7)?,
112            })
113        }).optional()?;
114        
115        Ok(project)
116    }
117    
118    pub fn archive_project(conn: &Connection, project_id: i64) -> Result<bool> {
119        let mut stmt = conn.prepare(
120            "UPDATE projects SET is_archived = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?1"
121        )?;
122        let changes = stmt.execute([project_id])?;
123        Ok(changes > 0)
124    }
125    
126    pub fn unarchive_project(conn: &Connection, project_id: i64) -> Result<bool> {
127        let mut stmt = conn.prepare(
128            "UPDATE projects SET is_archived = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?1"
129        )?;
130        let changes = stmt.execute([project_id])?;
131        Ok(changes > 0)
132    }
133    
134    pub fn update_project_path(conn: &Connection, project_id: i64, new_path: &PathBuf) -> Result<bool> {
135        let mut stmt = conn.prepare(
136            "UPDATE projects SET path = ?1, updated_at = CURRENT_TIMESTAMP WHERE id = ?2"
137        )?;
138        let changes = stmt.execute([new_path.to_string_lossy().to_string(), project_id.to_string()])?;
139        Ok(changes > 0)
140    }
141    
142    pub fn update_project_description(conn: &Connection, project_id: i64, description: Option<String>) -> Result<bool> {
143        let mut stmt = conn.prepare(
144            "UPDATE projects SET description = ?1, updated_at = CURRENT_TIMESTAMP WHERE id = ?2"
145        )?;
146        let changes = stmt.execute([description, Some(project_id.to_string())])?;
147        Ok(changes > 0)
148    }
149    
150    pub fn delete_project(conn: &Connection, project_id: i64) -> Result<bool> {
151        let mut stmt = conn.prepare("DELETE FROM projects WHERE id = ?1")?;
152        let changes = stmt.execute([project_id])?;
153        Ok(changes > 0)
154    }
155    
156    pub fn get_project_stats(conn: &Connection, project_id: i64) -> Result<Option<(i64, i64, i64)>> { // (total_sessions, total_time_seconds, avg_session_seconds)
157        let mut stmt = conn.prepare(
158            "SELECT 
159                COUNT(*) as session_count,
160                COALESCE(SUM(CASE 
161                    WHEN end_time IS NOT NULL THEN 
162                        (julianday(end_time) - julianday(start_time)) * 86400 - paused_duration
163                    ELSE 0
164                END), 0) as total_time,
165                COALESCE(AVG(CASE 
166                    WHEN end_time IS NOT NULL THEN 
167                        (julianday(end_time) - julianday(start_time)) * 86400 - paused_duration
168                    ELSE 0
169                END), 0) as avg_time
170             FROM sessions WHERE project_id = ?1 AND end_time IS NOT NULL"
171        )?;
172        
173        let stats = stmt.query_row([project_id], |row| {
174            Ok((
175                row.get::<_, i64>(0)?,
176                row.get::<_, f64>(1)? as i64,
177                row.get::<_, f64>(2)? as i64,
178            ))
179        }).optional()?;
180        
181        Ok(stats)
182    }
183}
184
185pub struct SessionQueries;
186
187impl SessionQueries {
188    pub fn create(conn: &Connection, session: &Session) -> Result<i64> {
189        let mut stmt = conn.prepare(
190            "INSERT INTO sessions (project_id, start_time, end_time, context, paused_duration, notes)
191             VALUES (?1, ?2, ?3, ?4, ?5, ?6)"
192        )?;
193        
194        stmt.execute(params![
195            session.project_id,
196            session.start_time,
197            session.end_time,
198            session.context.to_string(),
199            session.paused_duration.num_seconds(),
200            session.notes
201        ])?;
202        
203        Ok(conn.last_insert_rowid())
204    }
205    
206    pub fn find_active_session(conn: &Connection) -> Result<Option<Session>> {
207        let mut stmt = conn.prepare(
208            "SELECT id, project_id, start_time, end_time, context, paused_duration, notes, created_at
209             FROM sessions WHERE end_time IS NULL LIMIT 1"
210        )?;
211        
212        let session = stmt.query_row([], |row| {
213            Ok(Session {
214                id: Some(row.get(0)?),
215                project_id: row.get(1)?,
216                start_time: row.get(2)?,
217                end_time: row.get(3)?,
218                context: row.get::<_, String>(4)?.parse().unwrap(),
219                paused_duration: chrono::Duration::seconds(row.get::<_, i64>(5)?),
220                notes: row.get(6)?,
221                created_at: row.get(7)?,
222            })
223        }).optional()?;
224        
225        Ok(session)
226    }
227    
228    pub fn end_session(conn: &Connection, session_id: i64) -> Result<()> {
229        let mut stmt = conn.prepare(
230            "UPDATE sessions SET end_time = CURRENT_TIMESTAMP WHERE id = ?1"
231        )?;
232        
233        stmt.execute([session_id])?;
234        Ok(())
235    }
236    
237    pub fn list_recent(conn: &Connection, limit: usize) -> Result<Vec<Session>> {
238        let mut stmt = conn.prepare(
239            "SELECT id, project_id, start_time, end_time, context, paused_duration, notes, created_at
240             FROM sessions ORDER BY start_time DESC LIMIT ?1"
241        )?;
242        
243        let sessions = stmt.query_map([limit], |row| {
244            Ok(Session {
245                id: Some(row.get(0)?),
246                project_id: row.get(1)?,
247                start_time: row.get(2)?,
248                end_time: row.get(3)?,
249                context: row.get::<_, String>(4)?.parse().unwrap(),
250                paused_duration: chrono::Duration::seconds(row.get::<_, i64>(5)?),
251                notes: row.get(6)?,
252                created_at: row.get(7)?,
253            })
254        })?.collect::<Result<Vec<_>, _>>()?;
255        
256        Ok(sessions)
257    }
258    
259    pub fn find_by_id(conn: &Connection, session_id: i64) -> Result<Option<Session>> {
260        let mut stmt = conn.prepare(
261            "SELECT id, project_id, start_time, end_time, context, paused_duration, notes, created_at
262             FROM sessions WHERE id = ?1"
263        )?;
264        
265        let session = stmt.query_row([session_id], |row| {
266            Ok(Session {
267                id: Some(row.get(0)?),
268                project_id: row.get(1)?,
269                start_time: row.get(2)?,
270                end_time: row.get(3)?,
271                context: row.get::<_, String>(4)?.parse().unwrap(),
272                paused_duration: chrono::Duration::seconds(row.get::<_, i64>(5)?),
273                notes: row.get(6)?,
274                created_at: row.get(7)?,
275            })
276        }).optional()?;
277        
278        Ok(session)
279    }
280    
281    pub fn update_session(conn: &Connection, session_id: i64, start_time: Option<chrono::DateTime<chrono::Utc>>, end_time: Option<Option<chrono::DateTime<chrono::Utc>>>, project_id: Option<i64>, notes: Option<Option<String>>) -> Result<()> {
282        let mut updates = Vec::new();
283        let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
284        
285        if let Some(st) = start_time {
286            updates.push("start_time = ?");
287            params.push(Box::new(st));
288        }
289        
290        if let Some(et) = end_time {
291            updates.push("end_time = ?");
292            params.push(Box::new(et));
293        }
294        
295        if let Some(pid) = project_id {
296            updates.push("project_id = ?");
297            params.push(Box::new(pid));
298        }
299        
300        if let Some(n) = notes {
301            updates.push("notes = ?");
302            params.push(Box::new(n));
303        }
304        
305        if updates.is_empty() {
306            return Ok(());
307        }
308        
309        params.push(Box::new(session_id));
310        
311        let sql = format!("UPDATE sessions SET {} WHERE id = ?", updates.join(", "));
312        let mut stmt = conn.prepare(&sql)?;
313        let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect();
314        stmt.execute(&param_refs[..])?;
315        
316        Ok(())
317    }
318    
319    pub fn delete_session(conn: &Connection, session_id: i64) -> Result<()> {
320        let mut stmt = conn.prepare("DELETE FROM sessions WHERE id = ?1")?;
321        stmt.execute([session_id])?;
322        Ok(())
323    }
324    
325    pub fn list_with_filter(conn: &Connection, project_id: Option<i64>, start_date: Option<chrono::NaiveDate>, end_date: Option<chrono::NaiveDate>, limit: Option<usize>) -> Result<Vec<Session>> {
326        let mut sql = "SELECT id, project_id, start_time, end_time, context, paused_duration, notes, created_at FROM sessions WHERE 1=1".to_string();
327        let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
328        
329        if let Some(pid) = project_id {
330            sql.push_str(" AND project_id = ?");
331            params.push(Box::new(pid));
332        }
333        
334        if let Some(sd) = start_date {
335            sql.push_str(" AND date(start_time) >= ?");
336            params.push(Box::new(sd.format("%Y-%m-%d").to_string()));
337        }
338        
339        if let Some(ed) = end_date {
340            sql.push_str(" AND date(start_time) <= ?");
341            params.push(Box::new(ed.format("%Y-%m-%d").to_string()));
342        }
343        
344        sql.push_str(" ORDER BY start_time DESC");
345        
346        if let Some(lim) = limit {
347            sql.push_str(" LIMIT ?");
348            params.push(Box::new(lim));
349        }
350        
351        let mut stmt = conn.prepare(&sql)?;
352        let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect();
353        
354        let sessions = stmt.query_map(&param_refs[..], |row| {
355            Ok(Session {
356                id: Some(row.get(0)?),
357                project_id: row.get(1)?,
358                start_time: row.get(2)?,
359                end_time: row.get(3)?,
360                context: row.get::<_, String>(4)?.parse().unwrap(),
361                paused_duration: chrono::Duration::seconds(row.get::<_, i64>(5)?),
362                notes: row.get(6)?,
363                created_at: row.get(7)?,
364            })
365        })?.collect::<Result<Vec<_>, _>>()?;
366        
367        Ok(sessions)
368    }
369    
370    pub fn bulk_update_project(conn: &Connection, session_ids: &[i64], new_project_id: i64) -> Result<usize> {
371        let placeholders = vec!["?"; session_ids.len()].join(",");
372        let sql = format!("UPDATE sessions SET project_id = ? WHERE id IN ({})", placeholders);
373        
374        let mut stmt = conn.prepare(&sql)?;
375        let mut params: Vec<&dyn rusqlite::ToSql> = vec![&new_project_id];
376        for id in session_ids {
377            params.push(id);
378        }
379        
380        let changes = stmt.execute(&params[..])?;
381        Ok(changes)
382    }
383    
384    pub fn bulk_delete(conn: &Connection, session_ids: &[i64]) -> Result<usize> {
385        let placeholders = vec!["?"; session_ids.len()].join(",");
386        let sql = format!("DELETE FROM sessions WHERE id IN ({})", placeholders);
387        
388        let mut stmt = conn.prepare(&sql)?;
389        let params: Vec<&dyn rusqlite::ToSql> = session_ids.iter().map(|id| id as &dyn rusqlite::ToSql).collect();
390        
391        let changes = stmt.execute(&params[..])?;
392        Ok(changes)
393    }
394    
395    pub fn merge_sessions(conn: &Connection, session_ids: &[i64], target_project_id: Option<i64>, notes: Option<String>) -> Result<i64> {
396        if session_ids.is_empty() {
397            return Err(anyhow::anyhow!("No sessions to merge"));
398        }
399        
400        // Get all sessions to merge
401        let placeholders = vec!["?"; session_ids.len()].join(",");
402        let sql = format!(
403            "SELECT id, project_id, start_time, end_time, context, paused_duration, notes, created_at 
404             FROM sessions WHERE id IN ({}) ORDER BY start_time", 
405            placeholders
406        );
407        
408        let mut stmt = conn.prepare(&sql)?;
409        let params: Vec<&dyn rusqlite::ToSql> = session_ids.iter().map(|id| id as &dyn rusqlite::ToSql).collect();
410        
411        let sessions: Result<Vec<Session>, _> = stmt.query_map(&params[..], |row| {
412            Ok(Session {
413                id: Some(row.get(0)?),
414                project_id: row.get(1)?,
415                start_time: row.get(2)?,
416                end_time: row.get(3)?,
417                context: row.get::<_, String>(4)?.parse().unwrap(),
418                paused_duration: chrono::Duration::seconds(row.get::<_, i64>(5)?),
419                notes: row.get(6)?,
420                created_at: row.get(7)?,
421            })
422        })?.collect();
423        
424        let sessions = sessions?;
425        if sessions.is_empty() {
426            return Err(anyhow::anyhow!("No valid sessions found to merge"));
427        }
428        
429        // Calculate merged session properties
430        let earliest_start = sessions.iter().map(|s| s.start_time).min().unwrap();
431        let latest_end = sessions.iter().filter_map(|s| s.end_time).max();
432        let total_paused = sessions.iter().map(|s| s.paused_duration).fold(chrono::Duration::zero(), |acc, d| acc + d);
433        let merged_project_id = target_project_id.unwrap_or(sessions[0].project_id);
434        let merged_context = sessions[0].context; // Use first session's context
435        
436        // Create merged session
437        let merged_session = Session {
438            id: None,
439            project_id: merged_project_id,
440            start_time: earliest_start,
441            end_time: latest_end,
442            context: merged_context,
443            paused_duration: total_paused,
444            notes,
445            created_at: chrono::Utc::now(),
446        };
447        
448        // Insert merged session
449        let merged_id = Self::create(conn, &merged_session)?;
450        
451        // Create audit records for the merge
452        for session in &sessions {
453            if let Some(session_id) = session.id {
454                SessionEditQueries::create_edit_record(
455                    conn,
456                    session_id,
457                    session.start_time,
458                    session.end_time,
459                    merged_session.start_time,
460                    merged_session.end_time,
461                    Some(format!("Merged into session {}", merged_id))
462                )?;
463            }
464        }
465        
466        // Delete original sessions
467        Self::bulk_delete(conn, session_ids)?;
468        
469        Ok(merged_id)
470    }
471    
472    pub fn split_session(conn: &Connection, session_id: i64, split_times: &[chrono::DateTime<chrono::Utc>], notes_list: Option<Vec<String>>) -> Result<Vec<i64>> {
473        // Get the original session
474        let original_session = Self::find_by_id(conn, session_id)?
475            .ok_or_else(|| anyhow::anyhow!("Session {} not found", session_id))?;
476        
477        if split_times.is_empty() {
478            return Err(anyhow::anyhow!("No split times provided"));
479        }
480        
481        // Validate split times are within session bounds
482        for &split_time in split_times {
483            if split_time <= original_session.start_time {
484                return Err(anyhow::anyhow!("Split time {} is before session start", split_time));
485            }
486            if let Some(end_time) = original_session.end_time {
487                if split_time >= end_time {
488                    return Err(anyhow::anyhow!("Split time {} is after session end", split_time));
489                }
490            }
491        }
492        
493        // Sort split times
494        let mut sorted_splits = split_times.to_vec();
495        sorted_splits.sort();
496        
497        let mut new_session_ids = Vec::new();
498        let mut current_start = original_session.start_time;
499        
500        // Create sessions for each split segment
501        for (i, &split_time) in sorted_splits.iter().enumerate() {
502            let segment_notes = notes_list.as_ref()
503                .and_then(|list| list.get(i))
504                .cloned()
505                .or_else(|| original_session.notes.clone());
506            
507            let split_session = Session {
508                id: None,
509                project_id: original_session.project_id,
510                start_time: current_start,
511                end_time: Some(split_time),
512                context: original_session.context,
513                paused_duration: chrono::Duration::zero(), // Reset paused duration for splits
514                notes: segment_notes,
515                created_at: chrono::Utc::now(),
516            };
517            
518            let split_id = Self::create(conn, &split_session)?;
519            new_session_ids.push(split_id);
520            current_start = split_time;
521        }
522        
523        // Create final segment (from last split to original end)
524        let final_notes = notes_list.as_ref()
525            .and_then(|list| list.get(sorted_splits.len()))
526            .cloned()
527            .or_else(|| original_session.notes.clone());
528        
529        let final_session = Session {
530            id: None,
531            project_id: original_session.project_id,
532            start_time: current_start,
533            end_time: original_session.end_time,
534            context: original_session.context,
535            paused_duration: chrono::Duration::zero(),
536            notes: final_notes,
537            created_at: chrono::Utc::now(),
538        };
539        
540        let final_id = Self::create(conn, &final_session)?;
541        new_session_ids.push(final_id);
542        
543        // Create audit record for the split
544        SessionEditQueries::create_edit_record(
545            conn,
546            session_id,
547            original_session.start_time,
548            original_session.end_time,
549            original_session.start_time,
550            original_session.end_time,
551            Some(format!("Split into sessions: {}", new_session_ids.iter().map(|id| id.to_string()).collect::<Vec<_>>().join(", ")))
552        )?;
553        
554        // Delete original session
555        Self::delete_session(conn, session_id)?;
556        
557        Ok(new_session_ids)
558    }
559}
560
561pub struct SessionEditQueries;
562
563impl SessionEditQueries {
564    pub fn create_edit_record(conn: &Connection, session_id: i64, original_start: chrono::DateTime<chrono::Utc>, original_end: Option<chrono::DateTime<chrono::Utc>>, new_start: chrono::DateTime<chrono::Utc>, new_end: Option<chrono::DateTime<chrono::Utc>>, reason: Option<String>) -> Result<i64> {
565        let mut stmt = conn.prepare(
566            "INSERT INTO session_edits (session_id, original_start_time, original_end_time, new_start_time, new_end_time, edit_reason)
567             VALUES (?1, ?2, ?3, ?4, ?5, ?6)"
568        )?;
569        
570        stmt.execute(params![
571            session_id,
572            original_start,
573            original_end,
574            new_start, 
575            new_end,
576            reason
577        ])?;
578        
579        Ok(conn.last_insert_rowid())
580    }
581    
582    pub fn list_session_edits(conn: &Connection, session_id: i64) -> Result<Vec<crate::models::SessionEdit>> {
583        let mut stmt = conn.prepare(
584            "SELECT id, session_id, original_start_time, original_end_time, new_start_time, new_end_time, edit_reason, created_at
585             FROM session_edits WHERE session_id = ?1 ORDER BY created_at DESC"
586        )?;
587        
588        let edits = stmt.query_map([session_id], |row| {
589            Ok(crate::models::SessionEdit {
590                id: Some(row.get(0)?),
591                session_id: row.get(1)?,
592                original_start_time: row.get(2)?,
593                original_end_time: row.get(3)?,
594                new_start_time: row.get(4)?,
595                new_end_time: row.get(5)?,
596                edit_reason: row.get(6)?,
597                created_at: row.get(7)?,
598            })
599        })?.collect::<Result<Vec<_>, _>>()?;
600        
601        Ok(edits)
602    }
603}
604
605pub struct TagQueries;
606
607impl TagQueries {
608    pub fn create(conn: &Connection, tag: &Tag) -> Result<i64> {
609        let mut stmt = conn.prepare(
610            "INSERT INTO tags (name, color, description) VALUES (?1, ?2, ?3)"
611        )?;
612        
613        stmt.execute(params![
614            tag.name,
615            tag.color,
616            tag.description
617        ])?;
618        
619        Ok(conn.last_insert_rowid())
620    }
621    
622    pub fn list_all(conn: &Connection) -> Result<Vec<Tag>> {
623        let mut stmt = conn.prepare(
624            "SELECT id, name, color, description, created_at FROM tags ORDER BY name"
625        )?;
626        
627        let tags = stmt.query_map([], |row| {
628            Ok(Tag {
629                id: Some(row.get(0)?),
630                name: row.get(1)?,
631                color: row.get(2)?,
632                description: row.get(3)?,
633                created_at: row.get(4)?,
634            })
635        })?.collect::<Result<Vec<_>, _>>()?;
636        
637        Ok(tags)
638    }
639    
640    pub fn find_by_name(conn: &Connection, name: &str) -> Result<Option<Tag>> {
641        let mut stmt = conn.prepare(
642            "SELECT id, name, color, description, created_at FROM tags WHERE name = ?1"
643        )?;
644        
645        let tag = stmt.query_row([name], |row| {
646            Ok(Tag {
647                id: Some(row.get(0)?),
648                name: row.get(1)?,
649                color: row.get(2)?,
650                description: row.get(3)?,
651                created_at: row.get(4)?,
652            })
653        }).optional()?;
654        
655        Ok(tag)
656    }
657    
658    pub fn delete_by_name(conn: &Connection, name: &str) -> Result<bool> {
659        let mut stmt = conn.prepare("DELETE FROM tags WHERE name = ?1")?;
660        let changes = stmt.execute([name])?;
661        Ok(changes > 0)
662    }
663    
664    pub fn update_tag(conn: &Connection, name: &str, color: Option<String>, description: Option<String>) -> Result<bool> {
665        let mut updates = Vec::new();
666        let mut params: Vec<&dyn rusqlite::ToSql> = Vec::new();
667        
668        if let Some(c) = &color {
669            updates.push("color = ?");
670            params.push(c);
671        }
672        
673        if let Some(d) = &description {
674            updates.push("description = ?");
675            params.push(d);
676        }
677        
678        if updates.is_empty() {
679            return Ok(false);
680        }
681        
682        params.push(&name);
683        
684        let sql = format!("UPDATE tags SET {} WHERE name = ?", updates.join(", "));
685        let mut stmt = conn.prepare(&sql)?;
686        let changes = stmt.execute(&params[..])?;
687        
688        Ok(changes > 0)
689    }
690}