Skip to main content

reddb_server/runtime/
impl_dml_chain.rs

1//! DML chain-integrity & integrity-tombstone helpers extracted from `impl_dml`.
2//!
3//! Behaviour-preserving move (issue #1634). Names and behaviour are unchanged
4//! from `impl_dml`; the only adjustment is `pub(super)` visibility on the one
5//! private method still called from the sibling `impl_dml` module
6//! (`is_chain_integrity_broken`) so its INSERT path keeps calling it by bare
7//! name.
8
9use super::*;
10use crate::storage::query::unified::UnifiedResult;
11
12impl RedDBRuntime {
13    /// Issue #524 — public read of the in-memory chain tip. Returns `None`
14    /// when the collection is not a chain or has no rows (pre-genesis). On a
15    /// cold cache the first call falls back to a one-time scan so the HTTP
16    /// `GET /collections/:name/chain-tip` handler stays consistent with the
17    /// INSERT path after a restart.
18    pub fn chain_tip_for_collection(
19        &self,
20        collection: &str,
21    ) -> Option<crate::runtime::blockchain_kind::ChainTipFull> {
22        let store = self.inner.db.store();
23        if !crate::runtime::blockchain_kind::is_chain(&store, collection) {
24            return None;
25        }
26        let mut cache = self.inner.chain_tip_cache.lock();
27        if let Some(existing) = cache.get(collection) {
28            return Some(existing.clone());
29        }
30        let scanned = crate::runtime::blockchain_kind::chain_tip_full(&store, collection)?;
31        cache.insert(collection.to_string(), scanned.clone());
32        Some(scanned)
33    }
34
35    /// Issue #525 — walks the chain end-to-end, recomputes each block's hash
36    /// against the stored fields, and returns the verification outcome.  On
37    /// `ok == false` the integrity flag is persisted and the in-memory cache
38    /// is updated so subsequent INSERTs surface `ChainIntegrityBroken`.
39    ///
40    /// Returns `None` when the collection is absent or not a `KIND blockchain`.
41    pub fn verify_chain_for_collection(
42        &self,
43        collection: &str,
44    ) -> Option<crate::runtime::blockchain_kind::VerifyChainOutcome> {
45        let store = self.inner.db.store();
46        let outcome = crate::runtime::blockchain_kind::verify_chain_outcome(&store, collection)?;
47        if !outcome.ok {
48            crate::runtime::blockchain_kind::persist_integrity_flag(&store, collection, true);
49            self.inner
50                .chain_integrity_broken
51                .lock()
52                .insert(collection.to_string(), true);
53        }
54        Some(outcome)
55    }
56
57    /// Issue #525 — admin clears the `ChainIntegrityBroken` flag so the chain
58    /// accepts INSERTs again.  Returns `false` when the collection is not a
59    /// chain.
60    pub fn clear_chain_integrity_flag(&self, collection: &str) -> bool {
61        let store = self.inner.db.store();
62        if !crate::runtime::blockchain_kind::is_chain(&store, collection) {
63            return false;
64        }
65        crate::runtime::blockchain_kind::persist_integrity_flag(&store, collection, false);
66        self.inner
67            .chain_integrity_broken
68            .lock()
69            .insert(collection.to_string(), false);
70        true
71    }
72
73    /// Issue #525 — INSERT-time check.  Combines in-memory cache (fast path)
74    /// with a one-time scan of `red_config` on cold start so the flag survives
75    /// restart.
76    pub(super) fn is_chain_integrity_broken(&self, collection: &str) -> bool {
77        {
78            let cache = self.inner.chain_integrity_broken.lock();
79            if let Some(v) = cache.get(collection) {
80                return *v;
81            }
82        }
83        let store = self.inner.db.store();
84        let persisted =
85            crate::runtime::blockchain_kind::is_integrity_broken_persisted(&store, collection)
86                .unwrap_or(false);
87        self.inner
88            .chain_integrity_broken
89            .lock()
90            .insert(collection.to_string(), persisted);
91        persisted
92    }
93
94    /// Issue #765 / S6 — lazily hydrate the integrity-tombstone cache from
95    /// `red_config` on first access. Returns `true` when at least one
96    /// tombstone range is present. Subsequent calls observe the cached state
97    /// flag (`1` empty / `2` present) and skip the store scan.
98    fn ensure_integrity_tombstones_loaded(&self) -> bool {
99        use std::sync::atomic::Ordering;
100        match self
101            .inner
102            .integrity_tombstones_state
103            .load(Ordering::Relaxed)
104        {
105            1 => return false,
106            2 => return true,
107            _ => {}
108        }
109        // Cold: load under the cache lock so a concurrent reader cannot
110        // observe a half-populated vector.
111        let mut guard = self.inner.integrity_tombstones.lock();
112        if self
113            .inner
114            .integrity_tombstones_state
115            .load(Ordering::Relaxed)
116            == 0
117        {
118            let ranges = crate::runtime::integrity_tombstone::load_ranges(&self.inner.db.store());
119            let present = !ranges.is_empty();
120            *guard = ranges;
121            self.inner
122                .integrity_tombstones_state
123                .store(if present { 2 } else { 1 }, Ordering::Relaxed);
124        }
125        self.inner
126            .integrity_tombstones_state
127            .load(Ordering::Relaxed)
128            == 2
129    }
130
131    /// Issue #765 / S6 — durably record an integrity tombstone over the
132    /// inclusive RID range `[lo, hi]` of `table` (the committed rows of an
133    /// input stream whose end-to-end SHA-256 digest did not match). The range
134    /// is persisted to `red_config` (survives restart) and folded into the
135    /// in-memory cache so the same process filters it immediately.
136    pub fn record_integrity_tombstone(&self, table: &str, lo: u64, hi: u64) {
137        use std::sync::atomic::Ordering;
138        self.ensure_integrity_tombstones_loaded();
139        let mut guard = self.inner.integrity_tombstones.lock();
140        guard.push(crate::runtime::integrity_tombstone::TombstoneRange::new(
141            table.to_string(),
142            lo,
143            hi,
144        ));
145        crate::runtime::integrity_tombstone::persist_ranges(&self.inner.db.store(), &guard);
146        self.inner
147            .integrity_tombstones_state
148            .store(2, Ordering::Relaxed);
149    }
150
151    /// Issue #765 / S6 — snapshot of the currently-cached tombstone ranges.
152    /// Intended for tests and forensic surfaces; the read path uses
153    /// [`Self::filter_integrity_tombstoned`] which avoids the clone.
154    pub fn integrity_tombstone_ranges(
155        &self,
156    ) -> Vec<crate::runtime::integrity_tombstone::TombstoneRange> {
157        self.ensure_integrity_tombstones_loaded();
158        self.inner.integrity_tombstones.lock().clone()
159    }
160
161    /// Issue #765 / S6 — drop tombstoned rows from a SELECT result in place.
162    /// Fast no-op (one relaxed atomic load) when no tombstone has ever been
163    /// recorded. Clears `pre_serialized_json` when any row is removed so the
164    /// fast-path JSON cannot leak a filtered row back onto the wire.
165    pub fn filter_integrity_tombstoned(&self, result: &mut UnifiedResult) {
166        if !self.ensure_integrity_tombstones_loaded() {
167            return;
168        }
169        let guard = self.inner.integrity_tombstones.lock();
170        if guard.is_empty() {
171            return;
172        }
173        let before = result.records.len();
174        result.records.retain(|record| {
175            !crate::runtime::integrity_tombstone::record_tombstoned(&guard, record)
176        });
177        if result.records.len() != before {
178            result.pre_serialized_json = None;
179        }
180    }
181}