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