Skip to main content

pelicode_db/
clean.rs

1//! Removing data that is no longer referenced.
2//!
3//! Indexing a package version again replaces its list of files; a file that no
4//! other version links to is then left behind with its functions and their
5//! features. They never appear in results, since they have no occurrence, but
6//! they take space.
7
8use serde::Serialize;
9
10use crate::{Database, Result};
11
12/// What [`Database::clean`] removed, or would remove.
13#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize)]
14pub struct CleanSummary {
15    /// Source files no package version links to.
16    pub files: usize,
17    /// Functions of those files.
18    pub functions: usize,
19}
20
21const ORPHANS: &str = "
22    DROP TABLE IF EXISTS temp.orphan_file;
23    CREATE TEMP TABLE orphan_file AS
24        SELECT id FROM source_file
25        WHERE id NOT IN (SELECT source_file_id FROM version_file);
26    DROP TABLE IF EXISTS temp.orphan_function;
27    CREATE TEMP TABLE orphan_function AS
28        SELECT id FROM function WHERE source_file_id IN (SELECT id FROM orphan_file);
29";
30
31impl Database {
32    /// The files and functions [`Database::clean`] would remove, without removing them.
33    ///
34    /// # Errors
35    ///
36    /// [`Error`](crate::Error) when the database cannot be read.
37    ///
38    /// # Examples
39    ///
40    /// See [`Database::clean`].
41    pub fn unreferenced(&self) -> Result<CleanSummary> {
42        let count = |sql: &str| -> Result<usize> {
43            let n: i64 = self.conn.query_row(sql, [], |r| r.get(0))?;
44            Ok(n as usize)
45        };
46        Ok(CleanSummary {
47            files: count(
48                "SELECT COUNT(*) FROM source_file
49                 WHERE id NOT IN (SELECT source_file_id FROM version_file)",
50            )?,
51            functions: count(
52                "SELECT COUNT(*) FROM function WHERE source_file_id IN (
53                     SELECT id FROM source_file
54                     WHERE id NOT IN (SELECT source_file_id FROM version_file))",
55            )?,
56        })
57    }
58
59    /// Removes the source files no package version links to, with their
60    /// functions and features, in one transaction. The hash statistics are
61    /// not affected, since they count linked files only.
62    ///
63    /// # Errors
64    ///
65    /// [`Error`](crate::Error) when the database cannot be read or written;
66    /// nothing is removed then.
67    ///
68    /// # Examples
69    ///
70    /// ```
71    /// use pelicode_core::{Config, Language, extract_bytes};
72    /// use pelicode_db::{Database, PackageInfo};
73    ///
74    /// let dir = tempfile::tempdir().unwrap();
75    /// let config = Config::default();
76    /// let mut db = Database::open(dir.path().join("pelicode.db"), &config).unwrap();
77    /// let package = PackageInfo {
78    ///     ecosystem: "local".into(),
79    ///     name: "demo".into(),
80    ///     version: "1.0".into(),
81    ///     ..PackageInfo::default()
82    /// };
83    /// let file = |source: &[u8]| extract_bytes("a.py".as_ref(), Language::Python, source, &config).unwrap();
84    ///
85    /// db.index(&package, &[file(b"def f():\n    return 1\n")]).unwrap();
86    /// // Indexing the same version again leaves the first file unreferenced.
87    /// db.index(&package, &[file(b"def g():\n    return 2\n")]).unwrap();
88    /// assert_eq!(db.unreferenced().unwrap().files, 1);
89    ///
90    /// let removed = db.clean().unwrap();
91    /// assert_eq!((removed.files, removed.functions), (1, 1));
92    /// assert_eq!(db.unreferenced().unwrap().files, 0);
93    /// assert_eq!(db.stats().unwrap().functions, 1);
94    /// ```
95    pub fn clean(&mut self) -> Result<CleanSummary> {
96        let tx = self.conn.transaction()?;
97        tx.execute_batch(ORPHANS)?;
98        let count = |table: &str| -> Result<usize> {
99            let n: i64 = tx.query_row(&format!("SELECT COUNT(*) FROM temp.{table}"), [], |r| {
100                r.get(0)
101            })?;
102            Ok(n as usize)
103        };
104        let summary = CleanSummary {
105            files: count("orphan_file")?,
106            functions: count("orphan_function")?,
107        };
108        tx.execute_batch(
109            "DELETE FROM lsh_bucket  WHERE function_id IN (SELECT id FROM temp.orphan_function);
110             DELETE FROM minhash     WHERE function_id IN (SELECT id FROM temp.orphan_function);
111             DELETE FROM fingerprint WHERE function_id IN (SELECT id FROM temp.orphan_function);
112             DELETE FROM exact_hash  WHERE function_id IN (SELECT id FROM temp.orphan_function);
113             DELETE FROM function    WHERE id IN (SELECT id FROM temp.orphan_function);
114             DELETE FROM source_file WHERE id IN (SELECT id FROM temp.orphan_file);
115             DROP TABLE temp.orphan_function;
116             DROP TABLE temp.orphan_file;",
117        )?;
118        tx.commit()?;
119        Ok(summary)
120    }
121
122    /// Gives the space of removed rows back to the file system (SQLite `VACUUM`),
123    /// writing the write-ahead log into the database before and after.
124    ///
125    /// # Errors
126    ///
127    /// [`Error`](crate::Error) when another connection uses the database, or
128    /// when there is not enough disk space for the temporary copy `VACUUM` makes.
129    ///
130    /// # Examples
131    ///
132    /// ```
133    /// use pelicode_core::Config;
134    /// use pelicode_db::Database;
135    ///
136    /// let dir = tempfile::tempdir().unwrap();
137    /// let db = Database::open(dir.path().join("pelicode.db"), &Config::default()).unwrap();
138    /// db.vacuum().unwrap();
139    /// ```
140    pub fn vacuum(&self) -> Result<()> {
141        self.conn
142            // In WAL mode, VACUUM writes the new pages to the log too.
143            .execute_batch(
144                "PRAGMA wal_checkpoint(TRUNCATE); VACUUM; PRAGMA wal_checkpoint(TRUNCATE);",
145            )?;
146        Ok(())
147    }
148}