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