Skip to main content

sqlite_core/
row_history.rs

1//! Per-table, per-rowid VERSION HISTORY over a `SQLite` database's WAL temporal
2//! model — the evidence-based "row history" (Phase 1 of the WAL-temporal work).
3//!
4//! [`Database::row_histories`](crate::Database::row_histories) walks each salt
5//! epoch's [`CommitSnapshot`](crate::CommitSnapshot)s in commit order, then the
6//! final live view, and for every rowid emits the sequence of distinct record
7//! values it held — an `INSERT`/`UPDATE`/`DELETE`/reinsert history reconstructed
8//! purely from the bytes, with NO timestamps (SQLite WAL carries no wall-clock
9//! time; `commit_seq` is LOGICAL order within an epoch only).
10//!
11//! The two correctness-critical transforms are pure and unit-testable here:
12//! [`build_rowid_versions`] collapses identical consecutive values into one
13//! version (labelled by the EARLIEST view it appeared in) and classifies each
14//! version's [`ViewState`] from evidence; the same walk detects rowid REUSE (a
15//! present→absent→present-with-a-different-record gap is a delete+reinsert, two
16//! entities, never one continuous update).
17
18use crate::Value;
19use std::collections::BTreeMap;
20
21/// Where a [`RowVersion`] came from. Logical provenance only — never a timestamp.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum VersionOrigin {
24    /// The current on-disk ⊕ WAL live view (the FINAL state).
25    Live,
26    /// A materializable WAL commit, addressed by its [`CommitId`](crate::CommitId).
27    Commit(crate::CommitId),
28    /// Free-space carved residue with no precise temporal position (freeblocks
29    /// persist across commits, so the carve is ORDER-UNKNOWN).
30    CarvedResidue,
31}
32
33/// The EVIDENCE-based state of a row version relative to the FINAL live view.
34///
35/// Strictly evidence, never intent: it records what the bytes show (present /
36/// changed-later / absent / carved), not what the user meant to do.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum ViewState {
39    /// This exact value is present and unchanged in the final live view — the
40    /// current row.
41    PresentInFinalView,
42    /// An earlier value that a later view replaced with a DIFFERENT value for the
43    /// same rowid.
44    ValueChangedLater,
45    /// The rowid's last value before it disappeared: absent from the final live
46    /// view (a deletion).
47    AbsentInFinalView,
48    /// A free-space carve — order-unknown residue, not a positioned commit value.
49    CarvedResidue,
50}
51
52/// One version of one row: a distinct record value the rowid held at some point,
53/// with its evidence-based classification and logical provenance.
54// The boolean flags are independent EVIDENCE bits (deleted / guessed / reused /
55// uncertain), each separately consumed by a renderer — not a hidden state machine
56// that a two-variant enum would clarify, so the excessive-bools lint is waived.
57#[allow(clippy::struct_excessive_bools)]
58#[derive(Debug, Clone, PartialEq)]
59pub struct RowVersion {
60    /// The rowid this version belongs to. `None` only for a carved residue whose
61    /// rowid was destroyed.
62    pub rowid: Option<i64>,
63    /// The decoded column values of this version, in column order.
64    pub values: Vec<Value>,
65    /// Logical provenance of the version.
66    pub origin: VersionOrigin,
67    /// The LOGICAL commit sequence (per salt epoch, monotonic WITHIN an epoch
68    /// only) of the EARLIEST view this value appeared in. `None` for the live
69    /// view and for carved residue (order-unknown).
70    pub commit_seq: Option<u32>,
71    /// Evidence-based view state.
72    pub view_state: ViewState,
73    /// Whether this version's rowid is absent from the final live view (a
74    /// deletion or a completed pre-reuse entity).
75    pub is_deleted: bool,
76    /// Whether this version was reconstructed/guessed rather than read directly
77    /// (carved residue with inferred attribution).
78    pub is_guessed: bool,
79    /// Whether this rowid was REUSED — present, then absent for ≥1 view, then
80    /// present again with a DIFFERENT record. Set on every version of a reused
81    /// rowid so a renderer never presents two entities as one continuous update.
82    pub rowid_reused: bool,
83    /// Whether this version's attribution is uncertain — its source view was
84    /// checksum-invalid residue, or its schema could not be reconstructed.
85    pub attribution_uncertain: bool,
86}
87
88/// The version history of one user table.
89#[derive(Debug, Clone, PartialEq)]
90pub struct TableHistory {
91    /// The table's `sqlite_master.name`.
92    pub table: String,
93    /// The table's column names (real names where the schema parsed, else generic).
94    pub columns: Vec<String>,
95    /// Whether this is a `WITHOUT ROWID` table — such tables have no rowid and are
96    /// not version-tracked here; `versions` is then empty (presence recorded only).
97    pub without_rowid: bool,
98    /// Every row version, sorted by `rowid` (`None` last), then `commit_seq`
99    /// ascending, with carved-residue (order-unknown) versions grouped after the
100    /// ordered versions of their rowid.
101    pub versions: Vec<RowVersion>,
102}
103
104/// One chronological VIEW of a single table: a rowid→values map plus the view's
105/// logical position and trust flags. The ordered list of these (commit views in
106/// epoch order, then the final live view last) is the input to
107/// [`build_rowid_versions`].
108// `is_final` / `checksum_valid` / `schema_known` are independent per-view trust
109// bits, not a state machine; the excessive-bools lint is waived for clarity.
110#[allow(clippy::struct_excessive_bools)]
111#[derive(Debug, Clone, PartialEq)]
112pub struct RowView {
113    /// LOGICAL commit sequence within the salt epoch (monotonic within an epoch
114    /// only). `None` for the final live view.
115    pub commit_seq: Option<u32>,
116    /// Whether this is the final live view (the current state).
117    pub is_final: bool,
118    /// Whether the source commit's checksum chain validated. `false` = residue:
119    /// rows first attributed here are flagged `attribution_uncertain`.
120    pub checksum_valid: bool,
121    /// Whether the table's schema could be reconstructed for this view. `false`
122    /// flags rows attributed here `attribution_uncertain`.
123    pub schema_known: bool,
124    /// This view's logical provenance — [`VersionOrigin::Live`] for the final
125    /// view, [`VersionOrigin::Commit`] (carrying the real [`CommitId`](crate::CommitId))
126    /// for a WAL commit view. Copied verbatim onto every version first seen here.
127    pub origin: VersionOrigin,
128    /// This view's rows: rowid → decoded values. A rowid absent from the map is
129    /// absent in this view.
130    pub rows: BTreeMap<i64, Vec<Value>>,
131}
132
133/// Build the ordered [`RowVersion`]s for ONE `rowid` by walking `views` (already
134/// in chronological order: epoch commits ascending, then the final live view).
135///
136/// Emits a version only when the value CHANGES (insert / update / delete /
137/// reinsert), collapsing identical consecutive values into one version labelled
138/// by the EARLIEST view it appeared in. View-state is evidence-based:
139/// `PresentInFinalView` when the value is the final live value; `ValueChangedLater`
140/// when a later view replaced it with a different value; `AbsentInFinalView` when
141/// the rowid's last value disappeared. Rowid REUSE (present → absent → present
142/// with a different record) flags every version of the rowid `rowid_reused` and
143/// treats the post-gap value as a fresh insert.
144///
145/// Pure: no I/O. The result is in chronological order for this rowid.
146#[must_use]
147pub fn build_rowid_versions(rowid: i64, views: &[RowView]) -> Vec<RowVersion> {
148    // ---- 1. Collapse the per-view presence sequence into maximal runs --------
149    // A "present run" is a maximal stretch of views holding the SAME value with
150    // no intervening absence; an absence (the rowid missing from a view's map)
151    // breaks a run even when the value later reappears identically. We record
152    // each present run's value, the EARLIEST view it appeared in (the label),
153    // that view's trust flags, and whether an absence GAP preceded it.
154    struct Run<'a> {
155        values: &'a [Value],
156        earliest_seq: Option<u32>,
157        origin: VersionOrigin,
158        uncertain: bool,
159        gap_before: bool,
160    }
161    let mut runs: Vec<Run> = Vec::new();
162    let mut seen_present = false;
163    let mut pending_gap = false;
164    for view in views {
165        match view.rows.get(&rowid) {
166            None => {
167                // An absence only counts as a gap once the rowid has appeared.
168                if seen_present {
169                    pending_gap = true;
170                }
171            }
172            Some(values) => {
173                let uncertain = !view.checksum_valid || !view.schema_known;
174                let extends = matches!(runs.last(), Some(r) if r.values == values.as_slice());
175                if extends && !pending_gap {
176                    // Same value, no gap: extend the current run (collapse).
177                } else if extends && pending_gap {
178                    // Same value across a gap: still the same record by evidence —
179                    // collapse into the existing run (not a reuse), clearing the
180                    // gap so it is not treated as a delete+reinsert.
181                } else {
182                    runs.push(Run {
183                        values,
184                        earliest_seq: view.commit_seq,
185                        origin: view.origin.clone(),
186                        uncertain,
187                        gap_before: pending_gap,
188                    });
189                }
190                seen_present = true;
191                pending_gap = false;
192            }
193        }
194    }
195
196    if runs.is_empty() {
197        return Vec::new();
198    }
199
200    // ---- 2. Final-view facts -------------------------------------------------
201    // The rowid's value in the final live view (None if absent there). Whether
202    // the LAST run reaches the final view (no trailing gap after it).
203    let final_values: Option<&[Value]> = views
204        .iter()
205        .rev()
206        .find(|v| v.is_final)
207        .and_then(|v| v.rows.get(&rowid))
208        .map(Vec::as_slice);
209    let present_in_final = final_values.is_some();
210
211    // ---- 3. Reuse detection --------------------------------------------------
212    // Reuse = a present run, a gap, then a DIFFERENT value. `gap_before` on any
213    // run past the first is exactly that signal (a same-value-across-gap run was
214    // collapsed in step 1, so it never carries gap_before here).
215    let rowid_reused = runs.iter().skip(1).any(|r| r.gap_before);
216
217    // ---- 4. Emit one RowVersion per run --------------------------------------
218    let last_idx = runs.len() - 1;
219    // A run is "ended by deletion" iff the rowid DISAPPEARED right after it — the
220    // NEXT run begins after a gap (reuse), or it is the last run and the rowid is
221    // absent in the final view. A run replaced by a different value with NO gap is
222    // a direct UPDATE (ValueChangedLater), not a deletion.
223    let next_gap: Vec<bool> = (0..runs.len())
224        .map(|i| runs.get(i + 1).is_some_and(|r| r.gap_before))
225        .collect();
226    runs.iter()
227        .enumerate()
228        .map(|(i, run)| {
229            let is_last = i == last_idx;
230            // The final value reaches the last run iff that run is present in the
231            // final view AND its value equals the final value.
232            let is_final_value = is_last && present_in_final && final_values == Some(run.values);
233            let deleted_here = next_gap[i] || (is_last && !present_in_final);
234            let view_state = if is_final_value {
235                ViewState::PresentInFinalView
236            } else if deleted_here {
237                // The rowid disappeared after this value — its last value before
238                // the gap/end is AbsentInFinalView (a completed deletion).
239                ViewState::AbsentInFinalView
240            } else {
241                // An earlier value later replaced by a different one (direct update).
242                ViewState::ValueChangedLater
243            };
244            let is_deleted = matches!(view_state, ViewState::AbsentInFinalView);
245            RowVersion {
246                rowid: Some(rowid),
247                values: run.values.to_vec(),
248                // The version's origin is the EARLIEST view it appeared in — the
249                // same view that supplies its `commit_seq` label.
250                origin: run.origin.clone(),
251                commit_seq: run.earliest_seq,
252                view_state,
253                is_deleted,
254                is_guessed: false,
255                rowid_reused,
256                attribution_uncertain: run.uncertain,
257            }
258        })
259        .collect()
260}
261
262/// Assemble the [`TableHistory`] for ONE table from its chronological `views`
263/// (commit views in epoch order, then the final live view last) and its schema.
264///
265/// `WITHOUT ROWID` tables have no rowid to key a history on, so they are recorded
266/// with `without_rowid = true` and NO versions (a renderer annotates their
267/// presence). For an ordinary table, every rowid that appears in any view is run
268/// through [`build_rowid_versions`]; the resulting versions are sorted by `rowid`
269/// then `commit_seq` ascending (a `None` `commit_seq` — the live or order-unknown
270/// value — sorts LAST within a rowid).
271///
272/// Pure: no I/O.
273#[must_use]
274pub fn table_history(
275    table: String,
276    columns: Vec<String>,
277    without_rowid: bool,
278    views: &[RowView],
279) -> TableHistory {
280    if without_rowid {
281        return TableHistory {
282            table,
283            columns,
284            without_rowid: true,
285            versions: Vec::new(),
286        };
287    }
288    // Every rowid seen across all views, ascending.
289    let mut rowids: std::collections::BTreeSet<i64> = std::collections::BTreeSet::new();
290    for v in views {
291        rowids.extend(v.rows.keys().copied());
292    }
293    let mut versions: Vec<RowVersion> = Vec::new();
294    for rowid in rowids {
295        versions.extend(build_rowid_versions(rowid, views));
296    }
297    sort_versions(&mut versions);
298    TableHistory {
299        table,
300        columns,
301        without_rowid: false,
302        versions,
303    }
304}
305
306/// Re-sort a table's versions into the canonical order after a caller has
307/// appended more (e.g. the forensic layer merging carved residue): by `rowid`
308/// (`None` last), then `commit_seq` ascending with `None` (live / order-unknown
309/// residue) grouped LAST within the rowid. Stable.
310pub fn sort_table_versions(versions: &mut [RowVersion]) {
311    sort_versions(versions);
312}
313
314/// Sort versions by `rowid` (`None` last), then `commit_seq` ascending with a
315/// `None` `commit_seq` (live / order-unknown carved residue) grouped LAST within
316/// the rowid. Stable, so equal keys keep chronological insertion order.
317fn sort_versions(versions: &mut [RowVersion]) {
318    versions.sort_by(|a, b| {
319        // rowid: Some(_) before None; within Some, ascending by id.
320        let ra = a.rowid;
321        let rb = b.rowid;
322        let rowid_ord = match (ra, rb) {
323            (Some(x), Some(y)) => x.cmp(&y),
324            (Some(_), None) => std::cmp::Ordering::Less,
325            (None, Some(_)) => std::cmp::Ordering::Greater,
326            (None, None) => std::cmp::Ordering::Equal,
327        };
328        rowid_ord.then_with(|| {
329            // commit_seq: Some(_) (positioned) before None (live/residue), then
330            // ascending within Some.
331            match (a.commit_seq, b.commit_seq) {
332                (Some(x), Some(y)) => x.cmp(&y),
333                (Some(_), None) => std::cmp::Ordering::Less,
334                (None, Some(_)) => std::cmp::Ordering::Greater,
335                (None, None) => std::cmp::Ordering::Equal,
336            }
337        })
338    });
339}