Skip to main content

aft/db/
backups.rs

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}