Skip to main content

convergio_backup/
schema.rs

1//! DB migrations for the backup module.
2//!
3//! Tables: backup_snapshots, backup_retention_rules, backup_purge_log.
4
5use convergio_types::extension::Migration;
6
7pub fn migrations() -> Vec<Migration> {
8    vec![Migration {
9        version: 1,
10        description: "backup tables",
11        up: "\
12CREATE TABLE IF NOT EXISTS backup_snapshots (
13    id          TEXT PRIMARY KEY,
14    path        TEXT NOT NULL,
15    size_bytes  INTEGER NOT NULL DEFAULT 0,
16    checksum    TEXT NOT NULL,
17    node        TEXT NOT NULL,
18    created_at  TEXT NOT NULL DEFAULT (datetime('now'))
19);
20CREATE INDEX IF NOT EXISTS idx_backup_snap_date
21    ON backup_snapshots(created_at);
22
23CREATE TABLE IF NOT EXISTS backup_retention_rules (
24    id                INTEGER PRIMARY KEY AUTOINCREMENT,
25    table_name        TEXT NOT NULL,
26    timestamp_column  TEXT NOT NULL DEFAULT 'created_at',
27    max_age_days      INTEGER NOT NULL,
28    org_id            TEXT NOT NULL DEFAULT '__global__',
29    created_at        TEXT NOT NULL DEFAULT (datetime('now')),
30    UNIQUE(table_name, org_id)
31);
32CREATE TABLE IF NOT EXISTS backup_purge_log (
33    id            INTEGER PRIMARY KEY AUTOINCREMENT,
34    table_name    TEXT NOT NULL,
35    rows_deleted  INTEGER NOT NULL,
36    cutoff_date   TEXT NOT NULL,
37    executed_at   TEXT NOT NULL DEFAULT (datetime('now'))
38);
39CREATE INDEX IF NOT EXISTS idx_purge_log_date
40    ON backup_purge_log(executed_at);",
41    }]
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47
48    #[test]
49    fn migrations_have_sequential_versions() {
50        let migs = migrations();
51        assert_eq!(migs.len(), 1);
52        assert_eq!(migs[0].version, 1);
53    }
54
55    #[test]
56    fn migrations_apply_to_sqlite() {
57        let conn = rusqlite::Connection::open_in_memory().unwrap();
58        for m in migrations() {
59            conn.execute_batch(m.up).unwrap();
60        }
61        let count: i64 = conn
62            .query_row(
63                "SELECT COUNT(*) FROM sqlite_master WHERE type='table' \
64                 AND name LIKE 'backup_%'",
65                [],
66                |r| r.get(0),
67            )
68            .unwrap();
69        assert_eq!(count, 3);
70    }
71
72    #[test]
73    fn retention_rules_table_has_unique_constraint() {
74        let conn = rusqlite::Connection::open_in_memory().unwrap();
75        for m in migrations() {
76            conn.execute_batch(m.up).unwrap();
77        }
78        conn.execute(
79            "INSERT INTO backup_retention_rules \
80             (table_name, max_age_days, org_id) \
81             VALUES ('audit_log', 365, '__global__')",
82            [],
83        )
84        .unwrap();
85        // Duplicate with same org_id should fail
86        let result = conn.execute(
87            "INSERT INTO backup_retention_rules \
88             (table_name, max_age_days, org_id) \
89             VALUES ('audit_log', 30, '__global__')",
90            [],
91        );
92        assert!(result.is_err());
93    }
94}