bob/
db.rs

1/*
2 * Copyright (c) 2025 Jonathan Perkin <jonathan@perkin.org.uk>
3 *
4 * Permission to use, copy, modify, and distribute this software for any
5 * purpose with or without fee is hereby granted, provided that the above
6 * copyright notice and this permission notice appear in all copies.
7 *
8 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
16
17//! SQLite database for caching scan and build results.
18//!
19//! Stores [`ScanIndex`] data per pkgpath to enable resuming interrupted scans.
20//! Stores [`BuildResult`] data per pkgname to enable resuming interrupted builds.
21//! Users should clear the database when pkgsrc is updated.
22
23use crate::build::BuildResult;
24use anyhow::{Context, Result};
25use indexmap::IndexMap;
26use pkgsrc::{PkgName, PkgPath, ScanIndex};
27use rusqlite::{Connection, params};
28use std::path::Path;
29use tracing::debug;
30
31/// SQLite database for scan result caching.
32pub struct Database {
33    conn: Connection,
34}
35
36impl Database {
37    /// Open or create a database at the given path.
38    pub fn open(path: &Path) -> Result<Self> {
39        if let Some(parent) = path.parent() {
40            std::fs::create_dir_all(parent)
41                .context("Failed to create database directory")?;
42        }
43        let conn = Connection::open(path).context("Failed to open database")?;
44        let db = Self { conn };
45        db.init()?;
46        Ok(db)
47    }
48
49    fn init(&self) -> Result<()> {
50        self.conn.execute_batch(
51            "CREATE TABLE IF NOT EXISTS scan (
52                pkgpath TEXT PRIMARY KEY,
53                data TEXT NOT NULL
54            );
55            CREATE TABLE IF NOT EXISTS build (
56                pkgname TEXT PRIMARY KEY,
57                data TEXT NOT NULL
58            )",
59        )?;
60        Ok(())
61    }
62
63    /// Store scan results for a pkgpath.
64    pub fn store_scan_pkgpath(
65        &self,
66        pkgpath: &str,
67        indexes: &[ScanIndex],
68    ) -> Result<()> {
69        let json = serde_json::to_string(indexes)?;
70        self.conn.execute(
71            "INSERT OR REPLACE INTO scan (pkgpath, data) VALUES (?1, ?2)",
72            params![pkgpath, json],
73        )?;
74        debug!(pkgpath, "Stored scan result");
75        Ok(())
76    }
77
78    /// Load all cached scan, preserving insertion order.
79    pub fn get_all_scan(&self) -> Result<IndexMap<PkgPath, Vec<ScanIndex>>> {
80        let mut stmt = self
81            .conn
82            .prepare("SELECT pkgpath, data FROM scan ORDER BY rowid")?;
83        let mut result = IndexMap::new();
84
85        let rows = stmt.query_map([], |row| {
86            let pkgpath: String = row.get(0)?;
87            let json: String = row.get(1)?;
88            Ok((pkgpath, json))
89        })?;
90
91        for row in rows {
92            let (pkgpath_str, json) = row?;
93            let pkgpath = PkgPath::new(&pkgpath_str)
94                .context("Invalid pkgpath in database")?;
95            let indexes: Vec<ScanIndex> = serde_json::from_str(&json)
96                .context("Failed to deserialize scan data")?;
97            result.insert(pkgpath, indexes);
98        }
99
100        Ok(result)
101    }
102
103    /// Count of cached pkgpaths.
104    pub fn count_scan(&self) -> Result<i64> {
105        self.conn
106            .query_row("SELECT COUNT(*) FROM scan", [], |row| row.get(0))
107            .context("Failed to count scan")
108    }
109
110    /// Clear all cached scan data.
111    pub fn clear_scan(&self) -> Result<()> {
112        self.conn.execute("DELETE FROM scan", [])?;
113        Ok(())
114    }
115
116    /// Store build result for a pkgname.
117    pub fn store_build_pkgname(
118        &self,
119        pkgname: &str,
120        result: &BuildResult,
121    ) -> Result<()> {
122        let json = serde_json::to_string(result)?;
123        self.conn.execute(
124            "INSERT OR REPLACE INTO build (pkgname, data) VALUES (?1, ?2)",
125            params![pkgname, json],
126        )?;
127        debug!(pkgname, "Stored build result");
128        Ok(())
129    }
130
131    /// Load all cached build results, preserving insertion order.
132    pub fn get_all_build(&self) -> Result<IndexMap<PkgName, BuildResult>> {
133        let mut stmt = self
134            .conn
135            .prepare("SELECT pkgname, data FROM build ORDER BY rowid")?;
136        let mut result = IndexMap::new();
137
138        let rows = stmt.query_map([], |row| {
139            let pkgname: String = row.get(0)?;
140            let json: String = row.get(1)?;
141            Ok((pkgname, json))
142        })?;
143
144        for row in rows {
145            let (pkgname_str, json) = row?;
146            let pkgname = PkgName::new(&pkgname_str);
147            let build_result: BuildResult = serde_json::from_str(&json)
148                .context("Failed to deserialize build data")?;
149            result.insert(pkgname, build_result);
150        }
151
152        Ok(result)
153    }
154
155    /// Count of cached build results.
156    pub fn count_build(&self) -> Result<i64> {
157        self.conn
158            .query_row("SELECT COUNT(*) FROM build", [], |row| row.get(0))
159            .context("Failed to count build")
160    }
161
162    /// Clear all cached build data.
163    pub fn clear_build(&self) -> Result<()> {
164        self.conn.execute("DELETE FROM build", [])?;
165        Ok(())
166    }
167}