Skip to main content

omgbase_store/
read.rs

1//! Reads this spec pins (`spec/store/README.md` §5.2, §6): the old tree for
2//! the matcher, the pool, and byte reconstruction from the live rows or a
3//! revision's Merkle root.
4
5use std::collections::HashMap;
6
7use omgbase_format::BlockKind;
8use omgbase_format::hash::{hex, norm_hash, sha256};
9use omgbase_format::text::{
10    join_texts, normalize_text, normalize_visible_text, uses_children_text,
11};
12use omgbase_reconcile::{MatchBlock, PoolEntry};
13use rusqlite::{Connection, OptionalExtension, params};
14
15use crate::error::Result;
16use crate::tree::{from_hex, parse_tree_entries};
17
18/// A blob's bytes as text; `""` when the hash is unknown (as the reference).
19pub fn blob_text(conn: &Connection, hash: &[u8]) -> Result<String> {
20    let bytes: Option<Vec<u8>> = conn
21        .query_row(
22            "SELECT bytes FROM blobs WHERE hash = ?1",
23            params![hash],
24            |r| r.get(0),
25        )
26        .optional()?;
27    Ok(bytes.map_or_else(String::new, |b| String::from_utf8_lossy(&b).into_owned()))
28}
29
30/// §6.1: `leading_trivia`, the frontmatter blob + `frontmatter_trivia` when
31/// the current revision has one, then every top-level live block's raw and
32/// trivia in `order_key` order. `None` for a tombstoned or unknown doc.
33pub fn reconstruct(conn: &Connection, doc_id: &str) -> Result<Option<String>> {
34    let doc: Option<(String, Option<String>, Option<String>)> = conn
35        .query_row(
36            "SELECT leading_trivia, frontmatter_trivia, current_rev FROM docs
37             WHERE doc_id = ?1 AND deleted_commit IS NULL",
38            params![doc_id],
39            |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?)),
40        )
41        .optional()?;
42    let Some((leading, fm_trivia, current_rev)) = doc else {
43        return Ok(None);
44    };
45    let mut out = leading;
46    if let Some(rev) = current_rev {
47        let fm: Option<Option<Vec<u8>>> = conn
48            .query_row(
49                "SELECT frontmatter_blob FROM revisions WHERE rev_id = ?1",
50                params![rev],
51                |r| r.get(0),
52            )
53            .optional()?;
54        if let Some(Some(hash)) = fm {
55            out.push_str(&blob_text(conn, &hash)?);
56            out.push_str(fm_trivia.as_deref().unwrap_or(""));
57        }
58    }
59    let mut stmt = conn.prepare(
60        "SELECT raw_hash, trivia_hash FROM blocks
61         WHERE doc_id = ?1 AND parent_block IS NULL AND deleted_commit IS NULL
62         ORDER BY order_key",
63    )?;
64    let rows = stmt.query_map(params![doc_id], |r| {
65        Ok((r.get::<_, Vec<u8>>(0)?, r.get::<_, Option<Vec<u8>>>(1)?))
66    })?;
67    for row in rows {
68        let (raw, trivia) = row?;
69        out.push_str(&blob_text(conn, &raw)?);
70        if let Some(t) = trivia {
71            out.push_str(&blob_text(conn, &t)?);
72        }
73    }
74    Ok(Some(out))
75}
76
77/// The result of [`read_at_revision`].
78#[derive(Clone, Debug, PartialEq, Eq)]
79pub struct RevisionRead {
80    /// The path as of that revision.
81    pub path: String,
82    pub content: String,
83    /// `sha256(content) == revisions.rendered_hash`: byte-exact when true.
84    pub rendered_hash_match: bool,
85}
86
87/// §6.2: the same assembly sourced from the revision's Merkle root (children
88/// not walked), with `leading_trivia`/`frontmatter_trivia` from the current
89/// doc row. `None` when the doc is not live or `rev_id` is not one of its
90/// revisions.
91pub fn read_at_revision(
92    conn: &Connection,
93    doc_id: &str,
94    rev_id: &str,
95) -> Result<Option<RevisionRead>> {
96    let doc: Option<(String, Option<String>)> = conn
97        .query_row(
98            "SELECT leading_trivia, frontmatter_trivia FROM docs WHERE doc_id = ?1 AND deleted_commit IS NULL",
99            params![doc_id],
100            |r| Ok((r.get(0)?, r.get(1)?)),
101        )
102        .optional()?;
103    let Some((leading, fm_trivia)) = doc else {
104        return Ok(None);
105    };
106    type RevRow = (Vec<u8>, Option<Vec<u8>>, Vec<u8>, String);
107    let rev: Option<RevRow> = conn
108        .query_row(
109            "SELECT root_tree, frontmatter_blob, rendered_hash, path FROM revisions WHERE rev_id = ?1 AND doc_id = ?2",
110            params![rev_id, doc_id],
111            |r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?)),
112        )
113        .optional()?;
114    let Some((root_tree, fm_blob, rendered_hash, path)) = rev else {
115        return Ok(None);
116    };
117    let mut out = leading;
118    if let Some(hash) = fm_blob {
119        out.push_str(&blob_text(conn, &hash)?);
120        out.push_str(fm_trivia.as_deref().unwrap_or(""));
121    }
122    let entries: Option<String> = conn
123        .query_row(
124            "SELECT entries FROM tree_nodes WHERE hash = ?1",
125            params![root_tree],
126            |r| r.get(0),
127        )
128        .optional()?;
129    if let Some(text) = entries {
130        for e in parse_tree_entries(&text)? {
131            out.push_str(&blob_text(conn, &from_hex(&e.raw_hash_hex)?)?);
132            if let Some(t) = e.trivia_hash_hex {
133                out.push_str(&blob_text(conn, &from_hex(&t)?)?);
134            }
135        }
136    }
137    let rendered_hash_match = sha256(out.as_bytes())[..] == rendered_hash[..];
138    Ok(Some(RevisionRead {
139        path,
140        content: out,
141        rendered_hash_match,
142    }))
143}
144
145struct StoredBlock {
146    block_id: String,
147    parent_block: Option<String>,
148    ordinal: i64,
149    kind: String,
150    raw_hash: Vec<u8>,
151}
152
153/// §5.2: the matcher's old side from the live `blocks` rows, `ORDER BY
154/// parent_block, ordinal` (pinned as built, §10), positional keys
155/// `(parent_key ?? "") + "/" + ordinal`, `text` recomputed by the
156/// spec/format §4.1 tree rule, `anchors = []`.
157pub fn load_old_match_blocks(conn: &Connection, doc_id: &str) -> Result<Vec<MatchBlock>> {
158    let rows: Vec<StoredBlock> = {
159        let mut stmt = conn.prepare(
160            "SELECT block_id, parent_block, ordinal, type, raw_hash
161             FROM blocks WHERE doc_id = ?1 AND deleted_commit IS NULL
162             ORDER BY parent_block, ordinal",
163        )?;
164        let it = stmt.query_map(params![doc_id], |r| {
165            Ok(StoredBlock {
166                block_id: r.get(0)?,
167                parent_block: r.get(1)?,
168                ordinal: r.get(2)?,
169                kind: r.get(3)?,
170                raw_hash: r.get(4)?,
171            })
172        })?;
173        it.collect::<std::result::Result<Vec<_>, _>>()?
174    };
175    let index_of: HashMap<&str, usize> = rows
176        .iter()
177        .enumerate()
178        .map(|(i, r)| (r.block_id.as_str(), i))
179        .collect();
180    // The effective parent: an unknown parent id counts as top level.
181    let parent_of: Vec<Option<usize>> = rows
182        .iter()
183        .map(|r| {
184            r.parent_block
185                .as_deref()
186                .and_then(|p| index_of.get(p).copied())
187        })
188        .collect();
189    let mut children: HashMap<Option<usize>, Vec<usize>> = HashMap::new();
190    for (i, p) in parent_of.iter().enumerate() {
191        children.entry(*p).or_default().push(i);
192    }
193    for list in children.values_mut() {
194        list.sort_by_key(|&i| rows[i].ordinal);
195    }
196    let raws: Vec<String> = rows
197        .iter()
198        .map(|r| blob_text(conn, &r.raw_hash))
199        .collect::<Result<_>>()?;
200
201    // Text top-down (quote depth) with children composed bottom-up.
202    fn text_of(
203        i: usize,
204        quote_depth: usize,
205        rows: &[StoredBlock],
206        raws: &[String],
207        children: &HashMap<Option<usize>, Vec<usize>>,
208        texts: &mut Vec<Option<String>>,
209    ) -> String {
210        let kind = rows[i].kind.parse::<BlockKind>().ok();
211        let kids = children.get(&Some(i)).map_or(&[][..], Vec::as_slice);
212        let child_depth = quote_depth + usize::from(kind == Some(BlockKind::Blockquote));
213        let child_texts: Vec<String> = kids
214            .iter()
215            .map(|&c| text_of(c, child_depth, rows, raws, children, texts))
216            .collect();
217        let text = match kind {
218            Some(k) if uses_children_text(k, !kids.is_empty()) => {
219                join_texts(child_texts.iter().map(String::as_str))
220            }
221            Some(k) => normalize_visible_text(&raws[i], k, quote_depth),
222            // An unknown (non-Markdown) kind: whitespace normalization only.
223            None => normalize_text(&raws[i]),
224        };
225        texts[i] = Some(text.clone());
226        text
227    }
228    let mut texts: Vec<Option<String>> = vec![None; rows.len()];
229    for &root in children.get(&None).map_or(&[][..], Vec::as_slice) {
230        text_of(root, 0, &rows, &raws, &children, &mut texts);
231    }
232
233    fn key_of(i: usize, parent_of: &[Option<usize>], rows: &[StoredBlock]) -> String {
234        let parent_key = parent_of[i].map(|p| key_of(p, parent_of, rows));
235        format!("{}/{}", parent_key.unwrap_or_default(), rows[i].ordinal)
236    }
237    Ok(rows
238        .iter()
239        .enumerate()
240        .map(|(i, r)| {
241            let text = texts[i].clone().unwrap_or_default();
242            MatchBlock {
243                id: Some(r.block_id.clone()),
244                kind: r.kind.clone(),
245                raw_hash: hex(&r.raw_hash),
246                norm_hash: hex(&norm_hash(&text)),
247                text,
248                anchors: Vec::new(),
249                parent_key: parent_of[i].map(|p| key_of(p, &parent_of, &rows)),
250                index: usize::try_from(r.ordinal).unwrap_or(0),
251                key: key_of(i, &parent_of, &rows),
252            }
253        })
254        .collect())
255}
256
257/// §5.1: the repo's pool rows with `expires_ts > ts`, in row order.
258pub fn load_pool(conn: &Connection, repo_id: &str, ts: &str) -> Result<Vec<PoolEntry>> {
259    let mut stmt = conn.prepare(
260        "SELECT block_id, raw_hash, norm_hash, type FROM resurrection_pool
261         WHERE repo_id = ?1 AND expires_ts > ?2 ORDER BY rowid",
262    )?;
263    let rows = stmt.query_map(params![repo_id, ts], |r| {
264        Ok(PoolEntry {
265            id: r.get(0)?,
266            raw_hash: hex(&r.get::<_, Vec<u8>>(1)?),
267            norm_hash: hex(&r.get::<_, Vec<u8>>(2)?),
268            kind: r.get(3)?,
269        })
270    })?;
271    Ok(rows.collect::<std::result::Result<Vec<_>, _>>()?)
272}