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        self.try_chain_tip_for_collection(collection).ok().flatten()
23    }
24
25    pub fn try_chain_tip_for_collection(
26        &self,
27        collection: &str,
28    ) -> RedDBResult<Option<crate::runtime::blockchain_kind::ChainTipFull>> {
29        let store = self.inner.db.store();
30        if !super::blockchain_kind::try_is_chain(&store, collection)? {
31            return Ok(None);
32        }
33        let mut cache = self.inner.chain_tip_cache.lock();
34        if let Some(existing) = cache.get(collection) {
35            return Ok(Some(existing.clone()));
36        }
37        let Some(scanned) = super::blockchain_kind::try_chain_tip_full(&store, collection)? else {
38            return Ok(None);
39        };
40        cache.insert(collection.to_string(), scanned.clone());
41        Ok(Some(scanned))
42    }
43
44    /// Verify every physical block, preserving storage failures as errors.
45    pub fn verify_chain_for_collection(
46        &self,
47        collection: &str,
48    ) -> Option<crate::runtime::blockchain_kind::VerifyChainOutcome> {
49        self.try_verify_chain_for_collection(collection)
50            .ok()
51            .flatten()
52    }
53
54    pub fn try_verify_chain_for_collection(
55        &self,
56        collection: &str,
57    ) -> RedDBResult<Option<crate::runtime::blockchain_kind::VerifyChainOutcome>> {
58        let store = self.inner.db.store();
59        let Some(outcome) = super::blockchain_kind::try_verify_chain_outcome(&store, collection)?
60        else {
61            return Ok(None);
62        };
63        if !outcome.ok {
64            // Refuse further local writes even if publishing the durable flag fails.
65            self.inner
66                .chain_integrity_broken
67                .lock()
68                .insert(collection.to_string(), true);
69            super::blockchain_kind::try_persist_integrity_flag(&store, collection, true)?;
70        }
71        Ok(Some(outcome))
72    }
73
74    pub fn clear_chain_integrity_flag(&self, collection: &str) -> bool {
75        self.try_clear_chain_integrity_flag(collection)
76            .unwrap_or(false)
77    }
78
79    pub fn try_clear_chain_integrity_flag(&self, collection: &str) -> RedDBResult<bool> {
80        let store = self.inner.db.store();
81        if !super::blockchain_kind::try_is_chain(&store, collection)? {
82            return Ok(false);
83        }
84        super::blockchain_kind::try_persist_integrity_flag(&store, collection, false)?;
85        self.inner
86            .chain_integrity_broken
87            .lock()
88            .insert(collection.to_string(), false);
89        Ok(true)
90    }
91
92    pub(super) fn is_chain_integrity_broken(&self, collection: &str) -> RedDBResult<bool> {
93        if let Some(broken) = self.inner.chain_integrity_broken.lock().get(collection) {
94            return Ok(*broken);
95        }
96        let persisted = super::blockchain_kind::try_is_integrity_broken_persisted(
97            &self.inner.db.store(),
98            collection,
99        )?
100        .unwrap_or(false);
101        self.inner
102            .chain_integrity_broken
103            .lock()
104            .insert(collection.to_string(), persisted);
105        Ok(persisted)
106    }
107
108    /// Issue #765 / S6 — lazily hydrate the integrity-tombstone cache from
109    /// `red_config` on first access. Returns `true` when at least one
110    /// tombstone range is present. Subsequent calls observe the cached state
111    /// flag (`1` empty / `2` present) and skip the store scan.
112    pub(crate) fn ensure_integrity_tombstones_loaded(&self) -> RedDBResult<bool> {
113        use std::sync::atomic::Ordering;
114        match self
115            .inner
116            .integrity_tombstones_state
117            .load(Ordering::Relaxed)
118        {
119            1 => return Ok(false),
120            2 => return Ok(true),
121            _ => {}
122        }
123        // Cold: load under the cache lock so a concurrent reader cannot
124        // observe a half-populated vector.
125        let mut guard = self.inner.integrity_tombstones.lock();
126        if self
127            .inner
128            .integrity_tombstones_state
129            .load(Ordering::Relaxed)
130            == 0
131        {
132            let ranges = crate::runtime::integrity_tombstone::load_ranges(&self.inner.db.store())?;
133            let present = !ranges.is_empty();
134            *guard = ranges;
135            self.inner
136                .integrity_tombstones_state
137                .store(if present { 2 } else { 1 }, Ordering::Relaxed);
138        }
139        Ok(self
140            .inner
141            .integrity_tombstones_state
142            .load(Ordering::Relaxed)
143            == 2)
144    }
145
146    /// Issue #765 / S6 — durably record an integrity tombstone over the
147    /// inclusive RID range `[lo, hi]` of `table` (the committed rows of an
148    /// input stream whose end-to-end SHA-256 digest did not match). The range
149    /// is persisted to `red_config` (survives restart) and folded into the
150    /// in-memory cache so the same process filters it immediately.
151    pub fn record_integrity_tombstone(&self, table: &str, lo: u64, hi: u64) -> RedDBResult<()> {
152        self.record_integrity_tombstones(&[
153            crate::runtime::integrity_tombstone::TombstoneRange::new(table, lo, hi),
154        ])
155    }
156
157    pub(crate) fn record_integrity_tombstones(
158        &self,
159        ranges: &[crate::runtime::integrity_tombstone::TombstoneRange],
160    ) -> RedDBResult<()> {
161        use std::sync::atomic::Ordering;
162        if ranges.is_empty() {
163            return Ok(());
164        }
165        if ranges.iter().any(|range| range.lo > range.hi) {
166            return Err(RedDBError::Query(
167                "integrity tombstone range is reversed".into(),
168            ));
169        }
170        self.ensure_integrity_tombstones_loaded()?;
171        let mut guard = self.inner.integrity_tombstones.lock();
172        guard.extend_from_slice(ranges);
173        // Quarantine in this process even when durable publication fails.
174        // The caller must report the persistence failure to the stream client.
175        self.inner
176            .integrity_tombstones_state
177            .store(2, Ordering::Relaxed);
178        crate::runtime::integrity_tombstone::persist_ranges(&self.inner.db.store(), &guard)?;
179        Ok(())
180    }
181
182    /// Issue #765 / S6 — snapshot of the currently-cached tombstone ranges.
183    /// Intended for tests and forensic surfaces; the read path uses
184    /// [`Self::filter_integrity_tombstoned`] which avoids the clone.
185    pub fn integrity_tombstone_ranges(
186        &self,
187    ) -> RedDBResult<Vec<crate::runtime::integrity_tombstone::TombstoneRange>> {
188        self.ensure_integrity_tombstones_loaded()?;
189        Ok(self.inner.integrity_tombstones.lock().clone())
190    }
191
192    /// Issue #765 / S6 — drop tombstoned rows from a SELECT result in place.
193    /// Fast no-op (one relaxed atomic load) when no tombstone has ever been
194    /// recorded. Clears `pre_serialized_json` when any row is removed so the
195    /// fast-path JSON cannot leak a filtered row back onto the wire.
196    pub fn filter_integrity_tombstoned(&self, result: &mut UnifiedResult) -> RedDBResult<()> {
197        if !self.ensure_integrity_tombstones_loaded()? {
198            return Ok(());
199        }
200        let guard = self.inner.integrity_tombstones.lock();
201        if guard.is_empty() {
202            return Ok(());
203        }
204        let before = result.records.len();
205        result.records.retain(|record| {
206            !crate::runtime::integrity_tombstone::record_tombstoned(&guard, record)
207        });
208        if result.records.len() != before {
209            result.pre_serialized_json = None;
210        }
211        Ok(())
212    }
213}