tempo_cli/db/
queries.rs

1use crate::models::{Project, Session, Tag};
2use anyhow::Result;
3use rusqlite::{params, Connection, OptionalExtension};
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
33            .query_row([path.to_string_lossy().to_string()], |row| {
34                Ok(Project {
35                    id: Some(row.get(0)?),
36                    name: row.get(1)?,
37                    path: PathBuf::from(row.get::<_, String>(2)?),
38                    git_hash: row.get(3)?,
39                    created_at: row.get(4)?,
40                    updated_at: row.get(5)?,
41                    is_archived: row.get(6)?,
42                    description: row.get(7)?,
43                })
44            })
45            .optional()?;
46
47        Ok(project)
48    }
49
50    pub fn list_all(conn: &Connection, include_archived: bool) -> Result<Vec<Project>> {
51        let sql = if include_archived {
52            "SELECT id, name, path, git_hash, created_at, updated_at, is_archived, description
53             FROM projects ORDER BY name"
54        } else {
55            "SELECT id, name, path, git_hash, created_at, updated_at, is_archived, description
56             FROM projects WHERE is_archived = 0 ORDER BY name"
57        };
58
59        let mut stmt = conn.prepare(sql)?;
60        let projects = stmt
61            .query_map([], |row| {
62                Ok(Project {
63                    id: Some(row.get(0)?),
64                    name: row.get(1)?,
65                    path: PathBuf::from(row.get::<_, String>(2)?),
66                    git_hash: row.get(3)?,
67                    created_at: row.get(4)?,
68                    updated_at: row.get(5)?,
69                    is_archived: row.get(6)?,
70                    description: row.get(7)?,
71                })
72            })?
73            .collect::<Result<Vec<_>, _>>()?;
74
75        Ok(projects)
76    }
77
78    pub fn find_by_id(conn: &Connection, project_id: i64) -> Result<Option<Project>> {
79        let mut stmt = conn.prepare(
80            "SELECT id, name, path, git_hash, created_at, updated_at, is_archived, description
81             FROM projects WHERE id = ?1",
82        )?;
83
84        let project = stmt
85            .query_row([project_id], |row| {
86                Ok(Project {
87                    id: Some(row.get(0)?),
88                    name: row.get(1)?,
89                    path: PathBuf::from(row.get::<_, String>(2)?),
90                    git_hash: row.get(3)?,
91                    created_at: row.get(4)?,
92                    updated_at: row.get(5)?,
93                    is_archived: row.get(6)?,
94                    description: row.get(7)?,
95                })
96            })
97            .optional()?;
98
99        Ok(project)
100    }
101
102    pub fn find_by_name(conn: &Connection, name: &str) -> Result<Option<Project>> {
103        let mut stmt = conn.prepare(
104            "SELECT id, name, path, git_hash, created_at, updated_at, is_archived, description
105             FROM projects WHERE name = ?1",
106        )?;
107
108        let project = stmt
109            .query_row([name], |row| {
110                Ok(Project {
111                    id: Some(row.get(0)?),
112                    name: row.get(1)?,
113                    path: PathBuf::from(row.get::<_, String>(2)?),
114                    git_hash: row.get(3)?,
115                    created_at: row.get(4)?,
116                    updated_at: row.get(5)?,
117                    is_archived: row.get(6)?,
118                    description: row.get(7)?,
119                })
120            })
121            .optional()?;
122
123        Ok(project)
124    }
125
126    pub fn archive_project(conn: &Connection, project_id: i64) -> Result<bool> {
127        let mut stmt = conn.prepare(
128            "UPDATE projects SET is_archived = 1, updated_at = CURRENT_TIMESTAMP WHERE id = ?1",
129        )?;
130        let changes = stmt.execute([project_id])?;
131        Ok(changes > 0)
132    }
133
134    pub fn unarchive_project(conn: &Connection, project_id: i64) -> Result<bool> {
135        let mut stmt = conn.prepare(
136            "UPDATE projects SET is_archived = 0, updated_at = CURRENT_TIMESTAMP WHERE id = ?1",
137        )?;
138        let changes = stmt.execute([project_id])?;
139        Ok(changes > 0)
140    }
141
142    pub fn update_project_path(
143        conn: &Connection,
144        project_id: i64,
145        new_path: &PathBuf,
146    ) -> Result<bool> {
147        let mut stmt = conn.prepare(
148            "UPDATE projects SET path = ?1, updated_at = CURRENT_TIMESTAMP WHERE id = ?2",
149        )?;
150        let changes = stmt.execute([
151            new_path.to_string_lossy().to_string(),
152            project_id.to_string(),
153        ])?;
154        Ok(changes > 0)
155    }
156
157    pub fn update_project_description(
158        conn: &Connection,
159        project_id: i64,
160        description: Option<String>,
161    ) -> Result<bool> {
162        let mut stmt = conn.prepare(
163            "UPDATE projects SET description = ?1, updated_at = CURRENT_TIMESTAMP WHERE id = ?2",
164        )?;
165        let changes = stmt.execute([description, Some(project_id.to_string())])?;
166        Ok(changes > 0)
167    }
168
169    pub fn delete_project(conn: &Connection, project_id: i64) -> Result<bool> {
170        let mut stmt = conn.prepare("DELETE FROM projects WHERE id = ?1")?;
171        let changes = stmt.execute([project_id])?;
172        Ok(changes > 0)
173    }
174
175    pub fn get_project_stats(
176        conn: &Connection,
177        project_id: i64,
178    ) -> Result<Option<(i64, i64, i64)>> {
179        // (total_sessions, total_time_seconds, avg_session_seconds)
180        let mut stmt = conn.prepare(
181            "SELECT 
182                COUNT(*) as session_count,
183                COALESCE(SUM(CASE 
184                    WHEN end_time IS NOT NULL THEN 
185                        (julianday(end_time) - julianday(start_time)) * 86400 - paused_duration
186                    ELSE 0
187                END), 0) as total_time,
188                COALESCE(AVG(CASE 
189                    WHEN end_time IS NOT NULL THEN 
190                        (julianday(end_time) - julianday(start_time)) * 86400 - paused_duration
191                    ELSE 0
192                END), 0) as avg_time
193             FROM sessions WHERE project_id = ?1 AND end_time IS NOT NULL",
194        )?;
195
196        let stats = stmt
197            .query_row([project_id], |row| {
198                Ok((
199                    row.get::<_, i64>(0)?,
200                    row.get::<_, f64>(1)? as i64,
201                    row.get::<_, f64>(2)? as i64,
202                ))
203            })
204            .optional()?;
205
206        Ok(stats)
207    }
208
209    pub fn update_name(conn: &Connection, project_id: i64, name: String) -> Result<bool> {
210        let mut stmt = conn.prepare(
211            "UPDATE projects SET name = ?1, updated_at = CURRENT_TIMESTAMP WHERE id = ?2",
212        )?;
213        let changes = stmt.execute([&name, &project_id.to_string()])?;
214        Ok(changes > 0)
215    }
216
217    pub fn update_archived(conn: &Connection, project_id: i64, archived: bool) -> Result<bool> {
218        let mut stmt = conn.prepare(
219            "UPDATE projects SET is_archived = ?1, updated_at = CURRENT_TIMESTAMP WHERE id = ?2",
220        )?;
221        let changes = stmt.execute(params![archived, project_id])?;
222        Ok(changes > 0)
223    }
224
225    pub fn list_recent_with_stats(
226        conn: &Connection,
227        limit: usize,
228    ) -> Result<Vec<(Project, i64, i64, Option<chrono::DateTime<chrono::Utc>>)>> {
229        // (Project, today_seconds, total_seconds, last_active)
230        let mut stmt = conn.prepare(
231            "SELECT 
232                p.id, p.name, p.path, p.git_hash, p.created_at, p.updated_at, p.is_archived, p.description,
233                COALESCE(SUM(CASE 
234                    WHEN date(s.start_time) = date('now') AND s.end_time IS NOT NULL THEN 
235                        (julianday(s.end_time) - julianday(s.start_time)) * 86400 - s.paused_duration
236                    ELSE 0
237                END), 0) as today_seconds,
238                COALESCE(SUM(CASE 
239                    WHEN s.end_time IS NOT NULL THEN 
240                        (julianday(s.end_time) - julianday(s.start_time)) * 86400 - s.paused_duration
241                    ELSE 0
242                END), 0) as total_seconds,
243                MAX(s.end_time) as last_active
244             FROM projects p
245             LEFT JOIN sessions s ON p.id = s.project_id
246             WHERE p.is_archived = 0
247             GROUP BY p.id
248             ORDER BY last_active DESC NULLS LAST
249             LIMIT ?1",
250        )?;
251
252        let projects = stmt
253            .query_map([limit], |row| {
254                let project = Project {
255                    id: Some(row.get(0)?),
256                    name: row.get(1)?,
257                    path: PathBuf::from(row.get::<_, String>(2)?),
258                    git_hash: row.get(3)?,
259                    created_at: row.get(4)?,
260                    updated_at: row.get(5)?,
261                    is_archived: row.get(6)?,
262                    description: row.get(7)?,
263                };
264                let today_seconds: f64 = row.get(8)?;
265                let total_seconds: f64 = row.get(9)?;
266                let last_active: Option<chrono::DateTime<chrono::Utc>> = row.get(10)?;
267
268                Ok((
269                    project,
270                    today_seconds as i64,
271                    total_seconds as i64,
272                    last_active,
273                ))
274            })?
275            .collect::<Result<Vec<_>, _>>()?;
276
277        Ok(projects)
278    }
279}
280
281pub struct SessionQueries;
282
283impl SessionQueries {
284    pub fn create(conn: &Connection, session: &Session) -> Result<i64> {
285        let mut stmt = conn.prepare(
286            "INSERT INTO sessions (project_id, start_time, end_time, context, paused_duration, notes)
287             VALUES (?1, ?2, ?3, ?4, ?5, ?6)"
288        )?;
289
290        stmt.execute(params![
291            session.project_id,
292            session.start_time,
293            session.end_time,
294            session.context.to_string(),
295            session.paused_duration.num_seconds(),
296            session.notes
297        ])?;
298
299        Ok(conn.last_insert_rowid())
300    }
301
302    pub fn find_active_session(conn: &Connection) -> Result<Option<Session>> {
303        let mut stmt = conn.prepare(
304            "SELECT id, project_id, start_time, end_time, context, paused_duration, notes, created_at
305             FROM sessions WHERE end_time IS NULL LIMIT 1"
306        )?;
307
308        let session = stmt
309            .query_row([], |row| {
310                Ok(Session {
311                    id: Some(row.get(0)?),
312                    project_id: row.get(1)?,
313                    start_time: row.get(2)?,
314                    end_time: row.get(3)?,
315                    context: row.get::<_, String>(4)?.parse().map_err(|_e| {
316                        rusqlite::Error::InvalidColumnType(
317                            4,
318                            "context".to_string(),
319                            rusqlite::types::Type::Text,
320                        )
321                    })?,
322                    paused_duration: chrono::Duration::seconds(row.get::<_, i64>(5)?),
323                    notes: row.get(6)?,
324                    created_at: row.get(7)?,
325                })
326            })
327            .optional()?;
328
329        Ok(session)
330    }
331
332    pub fn end_session(conn: &Connection, session_id: i64) -> Result<()> {
333        let mut stmt =
334            conn.prepare("UPDATE sessions SET end_time = CURRENT_TIMESTAMP WHERE id = ?1")?;
335
336        stmt.execute([session_id])?;
337        Ok(())
338    }
339
340    pub fn list_recent(conn: &Connection, limit: usize) -> Result<Vec<Session>> {
341        let mut stmt = conn.prepare(
342            "SELECT id, project_id, start_time, end_time, context, paused_duration, notes, created_at
343             FROM sessions ORDER BY start_time DESC LIMIT ?1"
344        )?;
345
346        let sessions = stmt
347            .query_map([limit], |row| {
348                Ok(Session {
349                    id: Some(row.get(0)?),
350                    project_id: row.get(1)?,
351                    start_time: row.get(2)?,
352                    end_time: row.get(3)?,
353                    context: row.get::<_, String>(4)?.parse().map_err(|_e| {
354                        rusqlite::Error::InvalidColumnType(
355                            4,
356                            "context".to_string(),
357                            rusqlite::types::Type::Text,
358                        )
359                    })?,
360                    paused_duration: chrono::Duration::seconds(row.get::<_, i64>(5)?),
361                    notes: row.get(6)?,
362                    created_at: row.get(7)?,
363                })
364            })?
365            .collect::<Result<Vec<_>, _>>()?;
366
367        Ok(sessions)
368    }
369
370    pub fn find_by_id(conn: &Connection, session_id: i64) -> Result<Option<Session>> {
371        let mut stmt = conn.prepare(
372            "SELECT id, project_id, start_time, end_time, context, paused_duration, notes, created_at
373             FROM sessions WHERE id = ?1"
374        )?;
375
376        let session = stmt
377            .query_row([session_id], |row| {
378                Ok(Session {
379                    id: Some(row.get(0)?),
380                    project_id: row.get(1)?,
381                    start_time: row.get(2)?,
382                    end_time: row.get(3)?,
383                    context: row.get::<_, String>(4)?.parse().map_err(|_e| {
384                        rusqlite::Error::InvalidColumnType(
385                            4,
386                            "context".to_string(),
387                            rusqlite::types::Type::Text,
388                        )
389                    })?,
390                    paused_duration: chrono::Duration::seconds(row.get::<_, i64>(5)?),
391                    notes: row.get(6)?,
392                    created_at: row.get(7)?,
393                })
394            })
395            .optional()?;
396
397        Ok(session)
398    }
399
400    pub fn update_session(
401        conn: &Connection,
402        session_id: i64,
403        start_time: Option<chrono::DateTime<chrono::Utc>>,
404        end_time: Option<Option<chrono::DateTime<chrono::Utc>>>,
405        project_id: Option<i64>,
406        notes: Option<Option<String>>,
407    ) -> Result<()> {
408        let mut updates = Vec::new();
409        let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
410
411        if let Some(st) = start_time {
412            updates.push("start_time = ?");
413            params.push(Box::new(st));
414        }
415
416        if let Some(et) = end_time {
417            updates.push("end_time = ?");
418            params.push(Box::new(et));
419        }
420
421        if let Some(pid) = project_id {
422            updates.push("project_id = ?");
423            params.push(Box::new(pid));
424        }
425
426        if let Some(n) = notes {
427            updates.push("notes = ?");
428            params.push(Box::new(n));
429        }
430
431        if updates.is_empty() {
432            return Ok(());
433        }
434
435        params.push(Box::new(session_id));
436
437        let sql = format!("UPDATE sessions SET {} WHERE id = ?", updates.join(", "));
438        let mut stmt = conn.prepare(&sql)?;
439        let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect();
440        stmt.execute(&param_refs[..])?;
441
442        Ok(())
443    }
444
445    pub fn delete_session(conn: &Connection, session_id: i64) -> Result<()> {
446        let mut stmt = conn.prepare("DELETE FROM sessions WHERE id = ?1")?;
447        stmt.execute([session_id])?;
448        Ok(())
449    }
450
451    pub fn list_with_filter(
452        conn: &Connection,
453        project_id: Option<i64>,
454        start_date: Option<chrono::NaiveDate>,
455        end_date: Option<chrono::NaiveDate>,
456        limit: Option<usize>,
457    ) -> Result<Vec<Session>> {
458        let mut sql = "SELECT id, project_id, start_time, end_time, context, paused_duration, notes, created_at FROM sessions WHERE 1=1".to_string();
459        let mut params: Vec<Box<dyn rusqlite::ToSql>> = Vec::new();
460
461        if let Some(pid) = project_id {
462            sql.push_str(" AND project_id = ?");
463            params.push(Box::new(pid));
464        }
465
466        if let Some(sd) = start_date {
467            sql.push_str(" AND date(start_time) >= ?");
468            params.push(Box::new(sd.format("%Y-%m-%d").to_string()));
469        }
470
471        if let Some(ed) = end_date {
472            sql.push_str(" AND date(start_time) <= ?");
473            params.push(Box::new(ed.format("%Y-%m-%d").to_string()));
474        }
475
476        sql.push_str(" ORDER BY start_time DESC");
477
478        if let Some(lim) = limit {
479            sql.push_str(" LIMIT ?");
480            params.push(Box::new(lim));
481        }
482
483        let mut stmt = conn.prepare(&sql)?;
484        let param_refs: Vec<&dyn rusqlite::ToSql> = params.iter().map(|p| p.as_ref()).collect();
485
486        let sessions = stmt
487            .query_map(&param_refs[..], |row| {
488                Ok(Session {
489                    id: Some(row.get(0)?),
490                    project_id: row.get(1)?,
491                    start_time: row.get(2)?,
492                    end_time: row.get(3)?,
493                    context: row.get::<_, String>(4)?.parse().map_err(|_e| {
494                        rusqlite::Error::InvalidColumnType(
495                            4,
496                            "context".to_string(),
497                            rusqlite::types::Type::Text,
498                        )
499                    })?,
500                    paused_duration: chrono::Duration::seconds(row.get::<_, i64>(5)?),
501                    notes: row.get(6)?,
502                    created_at: row.get(7)?,
503                })
504            })?
505            .collect::<Result<Vec<_>, _>>()?;
506
507        Ok(sessions)
508    }
509
510    pub fn bulk_update_project(
511        conn: &Connection,
512        session_ids: &[i64],
513        new_project_id: i64,
514    ) -> Result<usize> {
515        let placeholders = vec!["?"; session_ids.len()].join(",");
516        let sql = format!(
517            "UPDATE sessions SET project_id = ? WHERE id IN ({})",
518            placeholders
519        );
520
521        let mut stmt = conn.prepare(&sql)?;
522        let mut params: Vec<&dyn rusqlite::ToSql> = vec![&new_project_id];
523        for id in session_ids {
524            params.push(id);
525        }
526
527        let changes = stmt.execute(&params[..])?;
528        Ok(changes)
529    }
530
531    pub fn bulk_delete(conn: &Connection, session_ids: &[i64]) -> Result<usize> {
532        let placeholders = vec!["?"; session_ids.len()].join(",");
533        let sql = format!("DELETE FROM sessions WHERE id IN ({})", placeholders);
534
535        let mut stmt = conn.prepare(&sql)?;
536        let params: Vec<&dyn rusqlite::ToSql> = session_ids
537            .iter()
538            .map(|id| id as &dyn rusqlite::ToSql)
539            .collect();
540
541        let changes = stmt.execute(&params[..])?;
542        Ok(changes)
543    }
544
545    pub fn merge_sessions(
546        conn: &Connection,
547        session_ids: &[i64],
548        target_project_id: Option<i64>,
549        notes: Option<String>,
550    ) -> Result<i64> {
551        if session_ids.is_empty() {
552            return Err(anyhow::anyhow!("No sessions to merge"));
553        }
554
555        // Get all sessions to merge
556        let placeholders = vec!["?"; session_ids.len()].join(",");
557        let sql = format!(
558            "SELECT id, project_id, start_time, end_time, context, paused_duration, notes, created_at 
559             FROM sessions WHERE id IN ({}) ORDER BY start_time", 
560            placeholders
561        );
562
563        let mut stmt = conn.prepare(&sql)?;
564        let params: Vec<&dyn rusqlite::ToSql> = session_ids
565            .iter()
566            .map(|id| id as &dyn rusqlite::ToSql)
567            .collect();
568
569        let sessions: Result<Vec<Session>, _> = stmt
570            .query_map(&params[..], |row| {
571                Ok(Session {
572                    id: Some(row.get(0)?),
573                    project_id: row.get(1)?,
574                    start_time: row.get(2)?,
575                    end_time: row.get(3)?,
576                    context: row.get::<_, String>(4)?.parse().map_err(|_e| {
577                        rusqlite::Error::InvalidColumnType(
578                            4,
579                            "context".to_string(),
580                            rusqlite::types::Type::Text,
581                        )
582                    })?,
583                    paused_duration: chrono::Duration::seconds(row.get::<_, i64>(5)?),
584                    notes: row.get(6)?,
585                    created_at: row.get(7)?,
586                })
587            })?
588            .collect();
589
590        let sessions = sessions?;
591        if sessions.is_empty() {
592            return Err(anyhow::anyhow!("No valid sessions found to merge"));
593        }
594
595        // Calculate merged session properties
596        let earliest_start =
597            sessions.iter().map(|s| s.start_time).min().ok_or_else(|| {
598                anyhow::anyhow!("No sessions found to determine earliest start time")
599            })?;
600        let latest_end = sessions.iter().filter_map(|s| s.end_time).max();
601        let total_paused = sessions
602            .iter()
603            .map(|s| s.paused_duration)
604            .fold(chrono::Duration::zero(), |acc, d| acc + d);
605        let merged_project_id = target_project_id.unwrap_or(sessions[0].project_id);
606        let merged_context = sessions[0].context; // Use first session's context
607
608        // Create merged session
609        let merged_session = Session {
610            id: None,
611            project_id: merged_project_id,
612            start_time: earliest_start,
613            end_time: latest_end,
614            context: merged_context,
615            paused_duration: total_paused,
616            notes,
617            created_at: chrono::Utc::now(),
618        };
619
620        // Insert merged session
621        let merged_id = Self::create(conn, &merged_session)?;
622
623        // Create audit records for the merge
624        for session in &sessions {
625            if let Some(session_id) = session.id {
626                SessionEditQueries::create_edit_record(
627                    conn,
628                    session_id,
629                    session.start_time,
630                    session.end_time,
631                    merged_session.start_time,
632                    merged_session.end_time,
633                    Some(format!("Merged into session {}", merged_id)),
634                )?;
635            }
636        }
637
638        // Delete original sessions
639        Self::bulk_delete(conn, session_ids)?;
640
641        Ok(merged_id)
642    }
643
644    pub fn split_session(
645        conn: &Connection,
646        session_id: i64,
647        split_times: &[chrono::DateTime<chrono::Utc>],
648        notes_list: Option<Vec<String>>,
649    ) -> Result<Vec<i64>> {
650        // Get the original session
651        let original_session = Self::find_by_id(conn, session_id)?
652            .ok_or_else(|| anyhow::anyhow!("Session {} not found", session_id))?;
653
654        if split_times.is_empty() {
655            return Err(anyhow::anyhow!("No split times provided"));
656        }
657
658        // Validate split times are within session bounds
659        for &split_time in split_times {
660            if split_time <= original_session.start_time {
661                return Err(anyhow::anyhow!(
662                    "Split time {} is before session start",
663                    split_time
664                ));
665            }
666            if let Some(end_time) = original_session.end_time {
667                if split_time >= end_time {
668                    return Err(anyhow::anyhow!(
669                        "Split time {} is after session end",
670                        split_time
671                    ));
672                }
673            }
674        }
675
676        // Sort split times
677        let mut sorted_splits = split_times.to_vec();
678        sorted_splits.sort();
679
680        let mut new_session_ids = Vec::new();
681        let mut current_start = original_session.start_time;
682
683        // Create sessions for each split segment
684        for (i, &split_time) in sorted_splits.iter().enumerate() {
685            let segment_notes = notes_list
686                .as_ref()
687                .and_then(|list| list.get(i))
688                .cloned()
689                .or_else(|| original_session.notes.clone());
690
691            let split_session = Session {
692                id: None,
693                project_id: original_session.project_id,
694                start_time: current_start,
695                end_time: Some(split_time),
696                context: original_session.context,
697                paused_duration: chrono::Duration::zero(), // Reset paused duration for splits
698                notes: segment_notes,
699                created_at: chrono::Utc::now(),
700            };
701
702            let split_id = Self::create(conn, &split_session)?;
703            new_session_ids.push(split_id);
704            current_start = split_time;
705        }
706
707        // Create final segment (from last split to original end)
708        let final_notes = notes_list
709            .as_ref()
710            .and_then(|list| list.get(sorted_splits.len()))
711            .cloned()
712            .or_else(|| original_session.notes.clone());
713
714        let final_session = Session {
715            id: None,
716            project_id: original_session.project_id,
717            start_time: current_start,
718            end_time: original_session.end_time,
719            context: original_session.context,
720            paused_duration: chrono::Duration::zero(),
721            notes: final_notes,
722            created_at: chrono::Utc::now(),
723        };
724
725        let final_id = Self::create(conn, &final_session)?;
726        new_session_ids.push(final_id);
727
728        // Create audit record for the split
729        SessionEditQueries::create_edit_record(
730            conn,
731            session_id,
732            original_session.start_time,
733            original_session.end_time,
734            original_session.start_time,
735            original_session.end_time,
736            Some(format!("Split into sessions: {:?}", new_session_ids)),
737        )?;
738
739        // Delete original session
740        Self::delete_session(conn, session_id)?;
741
742        Ok(new_session_ids)
743    }
744
745    pub fn list_by_date_range(
746        conn: &Connection,
747        from: chrono::DateTime<chrono::Utc>,
748        to: chrono::DateTime<chrono::Utc>,
749    ) -> Result<Vec<Session>> {
750        let mut stmt = conn.prepare(
751            "SELECT id, project_id, start_time, end_time, context, paused_duration, notes, created_at
752             FROM sessions 
753             WHERE start_time >= ?1 AND start_time <= ?2
754             ORDER BY start_time DESC"
755        )?;
756
757        let sessions = stmt
758            .query_map([from, to], |row| {
759                Ok(Session {
760                    id: Some(row.get(0)?),
761                    project_id: row.get(1)?,
762                    start_time: row.get(2)?,
763                    end_time: row.get(3)?,
764                    context: row.get::<_, String>(4)?.parse().map_err(|_e| {
765                        rusqlite::Error::InvalidColumnType(
766                            4,
767                            "context".to_string(),
768                            rusqlite::types::Type::Text,
769                        )
770                    })?,
771                    paused_duration: chrono::Duration::seconds(row.get::<_, i64>(5)?),
772                    notes: row.get(6)?,
773                    created_at: row.get(7)?,
774                })
775            })?
776            .collect::<Result<Vec<_>, _>>()?;
777
778        Ok(sessions)
779    }
780
781    pub fn get_daily_stats(conn: &Connection, date: chrono::NaiveDate) -> Result<(i64, i64, i64)> {
782        let sessions = Self::list_with_filter(conn, None, Some(date), Some(date), None)?;
783        let completed_sessions: Vec<_> = sessions
784            .into_iter()
785            .filter(|s| s.end_time.is_some())
786            .collect();
787
788        let sessions_count = completed_sessions.len() as i64;
789        let total_seconds: i64 = completed_sessions
790            .iter()
791            .map(|s| s.current_active_duration().num_seconds())
792            .sum();
793
794        let avg_seconds = if sessions_count > 0 {
795            total_seconds / sessions_count
796        } else {
797            0
798        };
799
800        Ok((sessions_count, total_seconds, avg_seconds))
801    }
802
803    pub fn get_weekly_stats(conn: &Connection, start_of_week: chrono::NaiveDate) -> Result<i64> {
804        let mut stmt = conn.prepare(
805            "SELECT 
806                COALESCE(SUM(CASE 
807                    WHEN end_time IS NOT NULL THEN 
808                        (julianday(end_time) - julianday(start_time)) * 86400 - paused_duration
809                    ELSE 0
810                END), 0) as total_seconds
811             FROM sessions 
812             WHERE date(start_time) >= ?1",
813        )?;
814
815        let total_seconds: f64 = stmt
816            .query_row([start_of_week.format("%Y-%m-%d").to_string()], |row| {
817                row.get(0)
818            })?;
819
820        Ok(total_seconds as i64)
821    }
822}
823
824pub struct SessionEditQueries;
825
826impl SessionEditQueries {
827    pub fn create_edit_record(
828        conn: &Connection,
829        session_id: i64,
830        original_start: chrono::DateTime<chrono::Utc>,
831        original_end: Option<chrono::DateTime<chrono::Utc>>,
832        new_start: chrono::DateTime<chrono::Utc>,
833        new_end: Option<chrono::DateTime<chrono::Utc>>,
834        reason: Option<String>,
835    ) -> Result<i64> {
836        let mut stmt = conn.prepare(
837            "INSERT INTO session_edits (session_id, original_start_time, original_end_time, new_start_time, new_end_time, edit_reason)
838             VALUES (?1, ?2, ?3, ?4, ?5, ?6)"
839        )?;
840
841        stmt.execute(params![
842            session_id,
843            original_start,
844            original_end,
845            new_start,
846            new_end,
847            reason
848        ])?;
849
850        Ok(conn.last_insert_rowid())
851    }
852
853    pub fn list_session_edits(
854        conn: &Connection,
855        session_id: i64,
856    ) -> Result<Vec<crate::models::SessionEdit>> {
857        let mut stmt = conn.prepare(
858            "SELECT id, session_id, original_start_time, original_end_time, new_start_time, new_end_time, edit_reason, created_at
859             FROM session_edits WHERE session_id = ?1 ORDER BY created_at DESC"
860        )?;
861
862        let edits = stmt
863            .query_map([session_id], |row| {
864                Ok(crate::models::SessionEdit {
865                    id: Some(row.get(0)?),
866                    session_id: row.get(1)?,
867                    original_start_time: row.get(2)?,
868                    original_end_time: row.get(3)?,
869                    new_start_time: row.get(4)?,
870                    new_end_time: row.get(5)?,
871                    edit_reason: row.get(6)?,
872                    created_at: row.get(7)?,
873                })
874            })?
875            .collect::<Result<Vec<_>, _>>()?;
876
877        Ok(edits)
878    }
879}
880
881pub struct TagQueries;
882
883impl TagQueries {
884    pub fn create(conn: &Connection, tag: &Tag) -> Result<i64> {
885        let mut stmt =
886            conn.prepare("INSERT INTO tags (name, color, description) VALUES (?1, ?2, ?3)")?;
887
888        stmt.execute(params![tag.name, tag.color, tag.description])?;
889
890        Ok(conn.last_insert_rowid())
891    }
892
893    pub fn list_all(conn: &Connection) -> Result<Vec<Tag>> {
894        let mut stmt = conn
895            .prepare("SELECT id, name, color, description, created_at FROM tags ORDER BY name")?;
896
897        let tags = stmt
898            .query_map([], |row| {
899                Ok(Tag {
900                    id: Some(row.get(0)?),
901                    name: row.get(1)?,
902                    color: row.get(2)?,
903                    description: row.get(3)?,
904                    created_at: row.get(4)?,
905                })
906            })?
907            .collect::<Result<Vec<_>, _>>()?;
908
909        Ok(tags)
910    }
911
912    pub fn find_by_name(conn: &Connection, name: &str) -> Result<Option<Tag>> {
913        let mut stmt = conn
914            .prepare("SELECT id, name, color, description, created_at FROM tags WHERE name = ?1")?;
915
916        let tag = stmt
917            .query_row([name], |row| {
918                Ok(Tag {
919                    id: Some(row.get(0)?),
920                    name: row.get(1)?,
921                    color: row.get(2)?,
922                    description: row.get(3)?,
923                    created_at: row.get(4)?,
924                })
925            })
926            .optional()?;
927
928        Ok(tag)
929    }
930
931    pub fn delete_by_name(conn: &Connection, name: &str) -> Result<bool> {
932        let mut stmt = conn.prepare("DELETE FROM tags WHERE name = ?1")?;
933        let changes = stmt.execute([name])?;
934        Ok(changes > 0)
935    }
936
937    pub fn update_tag(
938        conn: &Connection,
939        name: &str,
940        color: Option<String>,
941        description: Option<String>,
942    ) -> Result<bool> {
943        let mut updates = Vec::new();
944        let mut params: Vec<&dyn rusqlite::ToSql> = Vec::new();
945
946        if let Some(c) = &color {
947            updates.push("color = ?");
948            params.push(c);
949        }
950
951        if let Some(d) = &description {
952            updates.push("description = ?");
953            params.push(d);
954        }
955
956        if updates.is_empty() {
957            return Ok(false);
958        }
959
960        params.push(&name);
961
962        let sql = format!("UPDATE tags SET {} WHERE name = ?", updates.join(", "));
963        let mut stmt = conn.prepare(&sql)?;
964        let changes = stmt.execute(&params[..])?;
965
966        Ok(changes > 0)
967    }
968}