Skip to main content

aft/db/
backups.rs

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}