Skip to main content

reddb_server/runtime/
impl_index_integrity.rs

1use super::*;
2
3/// A derived checkpoint index which does not match its authoritative source.
4#[derive(Debug)]
5pub struct IndexIntegrityFinding {
6    pub index: String,
7    pub reason: String,
8}
9
10impl RedDBRuntime {
11    /// Verify HASH/BTREE checkpoint postings against persisted source rows.
12    ///
13    /// This explicit, potentially expensive pass does not modify data or repair
14    /// indexes. It checks the checkpoint generation, not uncheckpointed WAL or
15    /// resident index overlays, and does not replace the offline physical scrub.
16    /// Other index methods are rejected rather than reported as verified.
17    pub fn verify_persisted_indexes(
18        &self,
19        collection: &str,
20    ) -> RedDBResult<Vec<IndexIntegrityFinding>> {
21        use super::index_store::IndexMethodKind;
22        let topology = self.index_store_ref().collection_topology_lock(collection);
23        let _topology = topology.read();
24        let indexes = self.index_store_ref().list_indices(collection);
25        let store = self.db().store();
26        let manager = store
27            .get_collection(collection)
28            .ok_or_else(|| RedDBError::NotFound(collection.into()))?;
29        if !store.is_paged()
30            || store.config().resident_memory_bytes_max.is_none()
31            || indexes.iter().any(|index| {
32                !matches!(index.method, IndexMethodKind::Hash | IndexMethodKind::BTree)
33            })
34        {
35            return Err(RedDBError::InvalidOperation(
36                "persisted index verification requires disk-resident paged HASH/BTREE indexes"
37                    .into(),
38            ));
39        }
40        let (_scratch, limit_bytes) = self.reserve_index_scan(&manager)?;
41        let mut findings = Vec::new();
42        for index in indexes {
43            let mut names = vec![index.name.clone()];
44            if index.method == IndexMethodKind::BTree && (index.columns.len() == 1 || index.unique)
45            {
46                names.push(super::paged_btree_index::equality_storage_name(&index.name));
47            }
48            for name in names {
49                if let Some(reason) =
50                    store.verify_persisted_secondary_index(collection, &name, limit_bytes)?
51                {
52                    findings.push(IndexIntegrityFinding {
53                        index: index.name.clone(),
54                        reason: format!("{name:?}: {reason}"),
55                    });
56                }
57            }
58        }
59        Ok(findings)
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn resident_serverless_indexes_reject_physical_projection_verification() {
69        let directory = tempfile::tempdir().expect("serverless index fixture");
70        let options = crate::api::RedDBOptions::persistent(directory.path().join("index.rdb"))
71            .with_memory_budget(64 * 1024 * 1024)
72            .with_storage_profile(
73                crate::storage::profile::StorageDeployPreset::Serverless.selection(),
74            )
75            .expect("serverless profile");
76        let runtime = RedDBRuntime::with_options(options).expect("serverless runtime");
77        runtime
78            .execute_query("CREATE TABLE records (id INTEGER)")
79            .expect("create table");
80        runtime
81            .execute_query("CREATE INDEX records_id ON records (id) USING HASH")
82            .expect("create index");
83
84        let error = runtime
85            .verify_persisted_indexes("records")
86            .expect_err("resident index has no physical projection to verify");
87        assert!(error.to_string().contains(
88            "persisted index verification requires disk-resident paged HASH/BTREE indexes"
89        ));
90    }
91}