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    /// Whether this value went ABSENT from ≥1 intermediate view and then
87    /// reappeared with the IDENTICAL record. Such a run collapses (it is not
88    /// rowid reuse — the same record by evidence), but the absence itself is
89    /// forensic evidence: a delete-then-reinsert of the same value (e.g. a message
90    /// deleted and re-sent). A LOW-CONFIDENCE signal — a transient absence in the
91    /// materialized commit views is indistinguishable from a genuine round-trip,
92    /// so it records what the bytes show, never intent (§4.2).
93    pub reinserted_after_gap: bool,
94}
95
96/// The version history of one user table.
97#[derive(Debug, Clone, PartialEq)]
98pub struct TableHistory {
99    /// The table's `sqlite_master.name`.
100    pub table: String,
101    /// The table's column names (real names where the schema parsed, else generic).
102    pub columns: Vec<String>,
103    /// Whether this is a `WITHOUT ROWID` table — such tables have no rowid and are
104    /// not version-tracked here; `versions` is then empty (presence recorded only),
105    /// and the live rows are carried in [`without_rowid_rows`](Self::without_rowid_rows).
106    pub without_rowid: bool,
107    /// Every row version, sorted by `rowid` (`None` last), then `commit_seq`
108    /// ascending, with carved-residue (order-unknown) versions grouped after the
109    /// ordered versions of their rowid.
110    pub versions: Vec<RowVersion>,
111    /// For a `WITHOUT ROWID` table (§1.4): its live rows read from the index
112    /// b-tree, in the table's column order. Empty for an ordinary table and for a
113    /// `WITHOUT ROWID` table whose rows could not be read. These have no version
114    /// history (the model is rowid-keyed); they are the present live state.
115    pub without_rowid_rows: Vec<Vec<Value>>,
116}
117
118/// One chronological VIEW of a single table: a rowid→values map plus the view's
119/// logical position and trust flags. The ordered list of these (commit views in
120/// epoch order, then the final live view last) is the input to
121/// [`build_rowid_versions`].
122// `is_final` / `checksum_valid` / `schema_known` are independent per-view trust
123// bits, not a state machine; the excessive-bools lint is waived for clarity.
124#[allow(clippy::struct_excessive_bools)]
125#[derive(Debug, Clone, PartialEq)]
126pub struct RowView {
127    /// LOGICAL commit sequence within the salt epoch (monotonic within an epoch
128    /// only). `None` for the final live view.
129    pub commit_seq: Option<u32>,
130    /// Whether this is the final live view (the current state).
131    pub is_final: bool,
132    /// Whether the source commit's checksum chain validated. `false` = residue:
133    /// rows first attributed here are flagged `attribution_uncertain`.
134    pub checksum_valid: bool,
135    /// Whether the table's schema could be reconstructed for this view. `false`
136    /// flags rows attributed here `attribution_uncertain`.
137    pub schema_known: bool,
138    /// This view's logical provenance — [`VersionOrigin::Live`] for the final
139    /// view, [`VersionOrigin::Commit`] (carrying the real [`CommitId`](crate::CommitId))
140    /// for a WAL commit view. Copied verbatim onto every version first seen here.
141    pub origin: VersionOrigin,
142    /// This view's rows: rowid → decoded values. A rowid absent from the map is
143    /// absent in this view.
144    pub rows: BTreeMap<i64, Vec<Value>>,
145}
146
147/// Build the ordered [`RowVersion`]s for ONE `rowid` by walking `views` (already
148/// in chronological order: epoch commits ascending, then the final live view).
149///
150/// Emits a version only when the value CHANGES (insert / update / delete /
151/// reinsert), collapsing identical consecutive values into one version labelled
152/// by the EARLIEST view it appeared in. View-state is evidence-based:
153/// `PresentInFinalView` when the value is the final live value; `ValueChangedLater`
154/// when a later view replaced it with a different value; `AbsentInFinalView` when
155/// the rowid's last value disappeared. Rowid REUSE (present → absent → present
156/// with a different record) flags every version of the rowid `rowid_reused` and
157/// treats the post-gap value as a fresh insert.
158///
159/// Pure: no I/O. The result is in chronological order for this rowid.
160#[must_use]
161pub fn build_rowid_versions(rowid: i64, views: &[RowView]) -> Vec<RowVersion> {
162    // ---- 1. Collapse the per-view presence sequence into maximal runs --------
163    // A "present run" is a maximal stretch of views holding the SAME value with
164    // no intervening absence; an absence (the rowid missing from a view's map)
165    // breaks a run even when the value later reappears identically. We record
166    // each present run's value, the EARLIEST view it appeared in (the label),
167    // that view's trust flags, and whether an absence GAP preceded it.
168    struct Run<'a> {
169        values: &'a [Value],
170        earliest_seq: Option<u32>,
171        origin: VersionOrigin,
172        uncertain: bool,
173        gap_before: bool,
174        // The value went absent then reappeared IDENTICAL within this run (§4.2):
175        // collapsed as one record, but the absence is recorded as evidence.
176        reappeared_after_gap: bool,
177    }
178    let mut runs: Vec<Run> = Vec::new();
179    let mut seen_present = false;
180    let mut pending_gap = false;
181    for view in views {
182        match view.rows.get(&rowid) {
183            None => {
184                // An absence only counts as a gap once the rowid has appeared.
185                if seen_present {
186                    pending_gap = true;
187                }
188            }
189            Some(values) => {
190                let uncertain = !view.checksum_valid || !view.schema_known;
191                let extends = matches!(runs.last(), Some(r) if r.values == values.as_slice());
192                if extends && !pending_gap {
193                    // Same value, no gap: extend the current run (collapse).
194                } else if extends && pending_gap {
195                    // Same value across a gap: still the same record by evidence —
196                    // collapse into the existing run (not a reuse), clearing the
197                    // gap so it is not treated as a delete+reinsert. Record the
198                    // absence as a low-confidence evidence bit (§4.2) rather than
199                    // discarding it silently.
200                    if let Some(r) = runs.last_mut() {
201                        r.reappeared_after_gap = true;
202                    }
203                } else {
204                    runs.push(Run {
205                        values,
206                        earliest_seq: view.commit_seq,
207                        origin: view.origin.clone(),
208                        uncertain,
209                        gap_before: pending_gap,
210                        reappeared_after_gap: false,
211                    });
212                }
213                seen_present = true;
214                pending_gap = false;
215            }
216        }
217    }
218
219    if runs.is_empty() {
220        return Vec::new();
221    }
222
223    // ---- 2. Final-view facts -------------------------------------------------
224    // The rowid's value in the final live view (None if absent there). Whether
225    // the LAST run reaches the final view (no trailing gap after it).
226    let final_values: Option<&[Value]> = views
227        .iter()
228        .rev()
229        .find(|v| v.is_final)
230        .and_then(|v| v.rows.get(&rowid))
231        .map(Vec::as_slice);
232    let present_in_final = final_values.is_some();
233
234    // ---- 3. Reuse detection --------------------------------------------------
235    // Reuse = a present run, a gap, then a DIFFERENT value. `gap_before` on any
236    // run past the first is exactly that signal (a same-value-across-gap run was
237    // collapsed in step 1, so it never carries gap_before here).
238    let rowid_reused = runs.iter().skip(1).any(|r| r.gap_before);
239
240    // ---- 4. Emit one RowVersion per run --------------------------------------
241    let last_idx = runs.len() - 1;
242    // A run is "ended by deletion" iff the rowid DISAPPEARED right after it — the
243    // NEXT run begins after a gap (reuse), or it is the last run and the rowid is
244    // absent in the final view. A run replaced by a different value with NO gap is
245    // a direct UPDATE (ValueChangedLater), not a deletion.
246    let next_gap: Vec<bool> = (0..runs.len())
247        .map(|i| runs.get(i + 1).is_some_and(|r| r.gap_before))
248        .collect();
249    runs.iter()
250        .enumerate()
251        .map(|(i, run)| {
252            let is_last = i == last_idx;
253            // The final value reaches the last run iff that run is present in the
254            // final view AND its value equals the final value.
255            let is_final_value = is_last && present_in_final && final_values == Some(run.values);
256            let deleted_here = next_gap[i] || (is_last && !present_in_final);
257            let view_state = if is_final_value {
258                ViewState::PresentInFinalView
259            } else if deleted_here {
260                // The rowid disappeared after this value — its last value before
261                // the gap/end is AbsentInFinalView (a completed deletion).
262                ViewState::AbsentInFinalView
263            } else {
264                // An earlier value later replaced by a different one (direct update).
265                ViewState::ValueChangedLater
266            };
267            let is_deleted = matches!(view_state, ViewState::AbsentInFinalView);
268            RowVersion {
269                rowid: Some(rowid),
270                values: run.values.to_vec(),
271                // The version's origin is the EARLIEST view it appeared in — the
272                // same view that supplies its `commit_seq` label.
273                origin: run.origin.clone(),
274                commit_seq: run.earliest_seq,
275                view_state,
276                is_deleted,
277                is_guessed: false,
278                rowid_reused,
279                attribution_uncertain: run.uncertain,
280                reinserted_after_gap: run.reappeared_after_gap,
281            }
282        })
283        .collect()
284}
285
286/// Assemble the [`TableHistory`] for ONE table from its chronological `views`
287/// (commit views in epoch order, then the final live view last) and its schema.
288///
289/// `WITHOUT ROWID` tables have no rowid to key a history on, so they are recorded
290/// with `without_rowid = true` and NO versions (a renderer annotates their
291/// presence). For an ordinary table, every rowid that appears in any view is run
292/// through [`build_rowid_versions`]; the resulting versions are sorted by `rowid`
293/// then `commit_seq` ascending (a `None` `commit_seq` — the live or order-unknown
294/// value — sorts LAST within a rowid).
295///
296/// Pure: no I/O.
297#[must_use]
298pub fn table_history(
299    table: String,
300    columns: Vec<String>,
301    without_rowid: bool,
302    views: &[RowView],
303) -> TableHistory {
304    if without_rowid {
305        // No rowid → no version history. The live rows (from the index b-tree) are
306        // filled in by the caller (see `Database::row_histories`), which has the
307        // reader; the pure builder leaves them empty.
308        return TableHistory {
309            table,
310            columns,
311            without_rowid: true,
312            versions: Vec::new(),
313            without_rowid_rows: Vec::new(),
314        };
315    }
316    // Every rowid seen across all views, ascending.
317    let mut rowids: std::collections::BTreeSet<i64> = std::collections::BTreeSet::new();
318    for v in views {
319        rowids.extend(v.rows.keys().copied());
320    }
321    let mut versions: Vec<RowVersion> = Vec::new();
322    for rowid in rowids {
323        versions.extend(build_rowid_versions(rowid, views));
324    }
325    sort_versions(&mut versions);
326    TableHistory {
327        table,
328        columns,
329        without_rowid: false,
330        versions,
331        without_rowid_rows: Vec::new(),
332    }
333}
334
335/// Re-sort a table's versions into the canonical order after a caller has
336/// appended more (e.g. the forensic layer merging carved residue): by `rowid`
337/// (`None` last), then `commit_seq` ascending with `None` (live / order-unknown
338/// residue) grouped LAST within the rowid. Stable.
339pub fn sort_table_versions(versions: &mut [RowVersion]) {
340    sort_versions(versions);
341}
342
343/// Sort versions by `rowid` (`None` last), then `commit_seq` ascending with a
344/// `None` `commit_seq` (live / order-unknown carved residue) grouped LAST within
345/// the rowid. Stable, so equal keys keep chronological insertion order.
346fn sort_versions(versions: &mut [RowVersion]) {
347    versions.sort_by(|a, b| {
348        // rowid: Some(_) before None; within Some, ascending by id.
349        let ra = a.rowid;
350        let rb = b.rowid;
351        let rowid_ord = match (ra, rb) {
352            (Some(x), Some(y)) => x.cmp(&y),
353            (Some(_), None) => std::cmp::Ordering::Less,
354            (None, Some(_)) => std::cmp::Ordering::Greater,
355            (None, None) => std::cmp::Ordering::Equal,
356        };
357        rowid_ord.then_with(|| {
358            // commit_seq: Some(_) (positioned) before None (live/residue), then
359            // ascending within Some.
360            match (a.commit_seq, b.commit_seq) {
361                (Some(x), Some(y)) => x.cmp(&y),
362                (Some(_), None) => std::cmp::Ordering::Less,
363                (None, Some(_)) => std::cmp::Ordering::Greater,
364                (None, None) => std::cmp::Ordering::Equal,
365            }
366        })
367    });
368}