1use rusqlite::{params, Connection, OptionalExtension, Row};
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub struct BackupRow {
5 pub backup_id: String,
6 pub harness: String,
7 pub session_id: String,
8 pub project_key: String,
9 pub op_id: Option<String>,
10 pub order: u128,
11 pub file_path: String,
12 pub path_hash: String,
13 pub backup_path: Option<String>,
14 pub kind: String,
15 pub description: String,
16 pub created_at: i64,
17 pub is_tombstone: bool,
18}
19
20pub fn upsert_backup(conn: &Connection, row: &BackupRow) -> rusqlite::Result<()> {
21 delete_backup_for_order(
22 conn,
23 &row.harness,
24 &row.session_id,
25 &row.path_hash,
26 row.order,
27 )?;
28 insert_backup(conn, row)
29}
30
31pub fn insert_backup(conn: &Connection, row: &BackupRow) -> rusqlite::Result<()> {
32 let order_blob = row.order.to_be_bytes();
33 conn.execute(
34 "INSERT INTO backups (
35 backup_id, harness, session_id, project_key, op_id, order_blob, file_path,
36 path_hash, backup_path, kind, description, created_at, is_tombstone
37 ) VALUES (
38 ?1, ?2, ?3, ?4, ?5, ?6, ?7,
39 ?8, ?9, ?10, ?11, ?12, ?13
40 )",
41 params![
42 row.backup_id,
43 row.harness,
44 row.session_id,
45 row.project_key,
46 row.op_id,
47 &order_blob[..],
48 row.file_path,
49 row.path_hash,
50 row.backup_path,
51 row.kind,
52 row.description,
53 row.created_at,
54 row.is_tombstone,
55 ],
56 )?;
57
58 Ok(())
59}
60
61pub fn delete_backup_for_order(
62 conn: &Connection,
63 harness: &str,
64 session_id: &str,
65 path_hash: &str,
66 order: u128,
67) -> rusqlite::Result<usize> {
68 let order_blob = order.to_be_bytes();
69 conn.execute(
70 "DELETE FROM backups
71 WHERE harness = ?1 AND session_id = ?2 AND path_hash = ?3 AND order_blob = ?4",
72 params![harness, session_id, path_hash, &order_blob[..]],
73 )
74}
75
76pub fn get_latest_backup(
77 conn: &Connection,
78 harness: &str,
79 session_id: &str,
80 path_hash: &str,
81) -> rusqlite::Result<Option<BackupRow>> {
82 conn.query_row(
83 "SELECT backup_id, harness, session_id, project_key, op_id, order_blob, file_path,
84 path_hash, backup_path, kind, description, created_at, is_tombstone
85 FROM backups
86 WHERE harness = ?1 AND session_id = ?2 AND path_hash = ?3
87 ORDER BY order_blob DESC
88 LIMIT 1",
89 params![harness, session_id, path_hash],
90 map_backup_row,
91 )
92 .optional()
93}
94
95pub fn list_backups(
96 conn: &Connection,
97 harness: &str,
98 session_id: &str,
99 path_hash: &str,
100) -> rusqlite::Result<Vec<BackupRow>> {
101 let mut stmt = conn.prepare(
102 "SELECT backup_id, harness, session_id, project_key, op_id, order_blob, file_path,
103 path_hash, backup_path, kind, description, created_at, is_tombstone
104 FROM backups
105 WHERE harness = ?1 AND session_id = ?2 AND path_hash = ?3
106 ORDER BY order_blob ASC",
107 )?;
108
109 let rows = stmt
110 .query_map(params![harness, session_id, path_hash], map_backup_row)?
111 .collect();
112 rows
113}
114
115pub fn list_backups_by_op(
116 conn: &Connection,
117 harness: &str,
118 session_id: &str,
119 op_id: &str,
120) -> rusqlite::Result<Vec<BackupRow>> {
121 let mut stmt = conn.prepare(
122 "SELECT backup_id, harness, session_id, project_key, op_id, order_blob, file_path,
123 path_hash, backup_path, kind, description, created_at, is_tombstone
124 FROM backups
125 WHERE harness = ?1 AND session_id = ?2 AND op_id = ?3
126 ORDER BY file_path ASC, order_blob ASC",
127 )?;
128
129 let rows = stmt
130 .query_map(params![harness, session_id, op_id], map_backup_row)?
131 .collect();
132 rows
133}
134
135pub fn get_latest_operation_backup(
136 conn: &Connection,
137 harness: &str,
138 session_id: &str,
139) -> rusqlite::Result<Option<BackupRow>> {
140 conn.query_row(
141 "SELECT backup_id, harness, session_id, project_key, op_id, order_blob, file_path,
142 path_hash, backup_path, kind, description, created_at, is_tombstone
143 FROM backups
144 WHERE harness = ?1 AND session_id = ?2 AND op_id IS NOT NULL
145 ORDER BY order_blob DESC
146 LIMIT 1",
147 params![harness, session_id],
148 map_backup_row,
149 )
150 .optional()
151}
152
153pub fn delete_backups_for_path(
154 conn: &Connection,
155 harness: &str,
156 session_id: &str,
157 path_hash: &str,
158) -> rusqlite::Result<usize> {
159 conn.execute(
160 "DELETE FROM backups WHERE harness = ?1 AND session_id = ?2 AND path_hash = ?3",
161 params![harness, session_id, path_hash],
162 )
163}
164
165fn map_backup_row(row: &Row<'_>) -> rusqlite::Result<BackupRow> {
166 let order_blob: Vec<u8> = row.get(5)?;
167 let order = order_from_blob(&order_blob).unwrap_or_default();
168 Ok(BackupRow {
169 backup_id: row.get::<_, Option<String>>(0)?.unwrap_or_default(),
170 harness: row.get(1)?,
171 session_id: row.get(2)?,
172 project_key: row.get(3)?,
173 op_id: row.get(4)?,
174 order,
175 file_path: row.get(6)?,
176 path_hash: row.get(7)?,
177 backup_path: row.get(8)?,
178 kind: row.get(9)?,
179 description: row.get::<_, Option<String>>(10)?.unwrap_or_default(),
180 created_at: row.get(11)?,
181 is_tombstone: row.get::<_, i64>(12)? != 0,
182 })
183}
184
185fn order_from_blob(blob: &[u8]) -> Option<u128> {
186 let bytes: [u8; 16] = blob.try_into().ok()?;
187 Some(u128::from_be_bytes(bytes))
188}