Skip to main content

usenet_dl/db/
history.rs

1//! History management operations.
2
3use crate::types::HistoryEntry;
4use crate::{Error, Result};
5
6use super::{Database, HistoryRow, NewHistoryEntry};
7
8impl Database {
9    /// Insert a download into history
10    ///
11    /// This is typically called when a download is completed (successfully or failed)
12    /// to create a historical record separate from the active downloads table.
13    pub async fn insert_history(&self, entry: &NewHistoryEntry) -> Result<i64> {
14        let result = sqlx::query(
15            r#"
16            INSERT INTO history (
17                name, category, destination, status, size_bytes,
18                download_time_secs, completed_at
19            )
20            VALUES (?, ?, ?, ?, ?, ?, ?)
21            "#,
22        )
23        .bind(&entry.name)
24        .bind(&entry.category)
25        .bind(
26            entry
27                .destination
28                .as_ref()
29                .and_then(|p| p.to_str().map(String::from)),
30        )
31        .bind(entry.status)
32        .bind(entry.size_bytes as i64)
33        .bind(entry.download_time_secs)
34        .bind(entry.completed_at)
35        .execute(&self.pool)
36        .await
37        .map_err(Error::Sqlx)?;
38
39        Ok(result.last_insert_rowid())
40    }
41
42    /// Query history with pagination and optional status filter
43    ///
44    /// Returns history entries ordered by completion time (most recent first).
45    /// Use limit and offset for pagination.
46    pub async fn query_history(
47        &self,
48        status_filter: Option<i32>,
49        limit: usize,
50        offset: usize,
51    ) -> Result<Vec<HistoryEntry>> {
52        let query = if let Some(status) = status_filter {
53            sqlx::query_as::<_, HistoryRow>(
54                r#"
55                SELECT id, name, category, destination, status, size_bytes,
56                       download_time_secs, completed_at
57                FROM history
58                WHERE status = ?
59                ORDER BY completed_at DESC
60                LIMIT ? OFFSET ?
61                "#,
62            )
63            .bind(status)
64            .bind(limit as i64)
65            .bind(offset as i64)
66        } else {
67            sqlx::query_as::<_, HistoryRow>(
68                r#"
69                SELECT id, name, category, destination, status, size_bytes,
70                       download_time_secs, completed_at
71                FROM history
72                ORDER BY completed_at DESC
73                LIMIT ? OFFSET ?
74                "#,
75            )
76            .bind(limit as i64)
77            .bind(offset as i64)
78        };
79
80        let rows = query.fetch_all(&self.pool).await.map_err(Error::Sqlx)?;
81
82        Ok(rows.into_iter().map(HistoryEntry::from).collect())
83    }
84
85    /// Count history entries (optionally filtered by status)
86    ///
87    /// Useful for pagination - returns total count of records matching the filter.
88    pub async fn count_history(&self, status_filter: Option<i32>) -> Result<i64> {
89        let count = if let Some(status) = status_filter {
90            sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM history WHERE status = ?")
91                .bind(status)
92                .fetch_one(&self.pool)
93                .await
94                .map_err(Error::Sqlx)?
95        } else {
96            sqlx::query_scalar::<_, i64>("SELECT COUNT(*) FROM history")
97                .fetch_one(&self.pool)
98                .await
99                .map_err(Error::Sqlx)?
100        };
101
102        Ok(count)
103    }
104
105    /// Delete history entries older than the specified timestamp
106    ///
107    /// Returns the number of records deleted.
108    /// This is useful for cleanup - e.g., delete history older than 30 days.
109    pub async fn delete_history_before(&self, before_timestamp: i64) -> Result<u64> {
110        let result = sqlx::query("DELETE FROM history WHERE completed_at < ?")
111            .bind(before_timestamp)
112            .execute(&self.pool)
113            .await
114            .map_err(Error::Sqlx)?;
115
116        Ok(result.rows_affected())
117    }
118
119    /// Delete history entries with a specific status
120    ///
121    /// Returns the number of records deleted.
122    /// This is useful for cleanup - e.g., delete all failed downloads from history.
123    pub async fn delete_history_by_status(&self, status: i32) -> Result<u64> {
124        let result = sqlx::query("DELETE FROM history WHERE status = ?")
125            .bind(status)
126            .execute(&self.pool)
127            .await
128            .map_err(Error::Sqlx)?;
129
130        Ok(result.rows_affected())
131    }
132
133    /// Clear all history
134    ///
135    /// Returns the number of records deleted.
136    /// This is a destructive operation - use with caution.
137    pub async fn clear_history(&self) -> Result<u64> {
138        let result = sqlx::query("DELETE FROM history")
139            .execute(&self.pool)
140            .await
141            .map_err(Error::Sqlx)?;
142
143        Ok(result.rows_affected())
144    }
145
146    /// Delete history entries with optional filters
147    ///
148    /// Returns the number of records deleted.
149    /// Supports filtering by:
150    /// - before_timestamp: Delete entries completed before this timestamp
151    /// - status: Delete only entries with this status
152    ///
153    /// If both filters are None, deletes all history (same as clear_history).
154    pub async fn delete_history_filtered(
155        &self,
156        before_timestamp: Option<i64>,
157        status: Option<i32>,
158    ) -> Result<u64> {
159        match (before_timestamp, status) {
160            (None, None) => {
161                // No filters - delete all
162                self.clear_history().await
163            }
164            (Some(before), None) => {
165                // Only timestamp filter
166                self.delete_history_before(before).await
167            }
168            (None, Some(status_val)) => {
169                // Only status filter
170                self.delete_history_by_status(status_val).await
171            }
172            (Some(before), Some(status_val)) => {
173                // Both filters
174                let result =
175                    sqlx::query("DELETE FROM history WHERE completed_at < ? AND status = ?")
176                        .bind(before)
177                        .bind(status_val)
178                        .execute(&self.pool)
179                        .await
180                        .map_err(Error::Sqlx)?;
181
182                Ok(result.rows_affected())
183            }
184        }
185    }
186
187    /// Get a single history entry by ID
188    pub async fn get_history_entry(&self, id: i64) -> Result<Option<HistoryEntry>> {
189        let row = sqlx::query_as::<_, HistoryRow>(
190            r#"
191            SELECT id, name, category, destination, status, size_bytes,
192                   download_time_secs, completed_at
193            FROM history
194            WHERE id = ?
195            "#,
196        )
197        .bind(id)
198        .fetch_optional(&self.pool)
199        .await
200        .map_err(Error::Sqlx)?;
201
202        Ok(row.map(HistoryEntry::from))
203    }
204}