Skip to main content

omgbase_store/
writers.rs

1//! Immutable writers (`spec/store/README.md` §4.1, §5.4 steps 4–6): blobs
2//! and tree nodes are content-addressed (`INSERT OR IGNORE` gives structural
3//! sharing); commits and revisions take the next per-repo / per-document
4//! sequence number.
5
6use std::collections::BTreeMap;
7
8use omgbase_format::hash::{hex, sha256};
9use omgbase_format::json::attrs_to_json;
10use omgbase_format::{Attrs, Block, Span};
11use rusqlite::{Connection, params};
12
13use crate::error::Result;
14use crate::mint::Mint;
15use crate::tree::{TreeEntry, from_hex, serialize_tree_entries};
16
17/// A block with its id, ready to encode into the tree and the `blocks` rows.
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct TreeInputBlock {
20    pub block_id: String,
21    /// The spec/format §3 kind name (`blocks.type`).
22    pub kind: String,
23    pub raw: String,
24    /// Visible text (spec/format §4.1), computed in tree context.
25    pub text: String,
26    /// Trailing trivia (top level only; `""` below).
27    pub trivia: String,
28    pub attrs: Attrs,
29    /// The block's source span (spec/format §1): what `own_text` uses to
30    /// blank a container's children when the properties and graph layers
31    /// scan (spec/properties §3.2, spec/graph §1). Not stored.
32    pub span: Span,
33    pub children: Vec<TreeInputBlock>,
34}
35
36/// Assign ids from a reconcile assignment (new positional key → id) onto a
37/// parsed body tree; a key the assignment misses mints `b` (the reference's
38/// `assignFromMap`; spec §5.4 step 3 says every key is assigned).
39pub fn assign_from_map(
40    blocks: &[Block],
41    assignment: &BTreeMap<String, String>,
42    minter: &mut Mint<'_>,
43) -> Result<Vec<TreeInputBlock>> {
44    fn walk(
45        list: &[Block],
46        parent_key: Option<&str>,
47        assignment: &BTreeMap<String, String>,
48        minter: &mut Mint<'_>,
49    ) -> Result<Vec<TreeInputBlock>> {
50        list.iter()
51            .enumerate()
52            .map(|(index, b)| {
53                let key = format!("{}/{index}", parent_key.unwrap_or(""));
54                let block_id = match assignment.get(&key) {
55                    Some(id) => id.clone(),
56                    None => minter.mint("b")?,
57                };
58                Ok(TreeInputBlock {
59                    block_id,
60                    kind: b.kind.as_str().to_owned(),
61                    raw: b.raw.clone(),
62                    text: b.text.clone(),
63                    trivia: b.trivia.clone(),
64                    attrs: b.attrs.clone(),
65                    span: b.span,
66                    children: walk(&b.children, Some(&key), assignment, minter)?,
67                })
68            })
69            .collect()
70    }
71    walk(blocks, None, assignment, minter)
72}
73
74/// Mint a fresh `b` id for every block (the re-mint path).
75pub fn assign_fresh_ids(blocks: &[Block], minter: &mut Mint<'_>) -> Result<Vec<TreeInputBlock>> {
76    assign_from_map(blocks, &BTreeMap::new(), minter)
77}
78
79/// Content-address `text`'s UTF-8 bytes into `blobs`; returns the hex hash.
80pub fn put_blob(conn: &Connection, text: &str) -> Result<String> {
81    let bytes = text.as_bytes();
82    let hash = sha256(bytes);
83    conn.execute(
84        "INSERT OR IGNORE INTO blobs (hash, bytes, size) VALUES (?1, ?2, ?3)",
85        params![&hash[..], bytes, bytes.len() as i64],
86    )?;
87    Ok(hex(&hash))
88}
89
90/// Content-address a node's entries into `tree_nodes`; returns the hex hash.
91pub fn put_tree_node(conn: &Connection, entries: &[TreeEntry]) -> Result<String> {
92    let serialized = serialize_tree_entries(entries);
93    let hash = sha256(serialized.as_bytes());
94    conn.execute(
95        "INSERT OR IGNORE INTO tree_nodes (hash, entries) VALUES (?1, ?2)",
96        params![&hash[..], serialized],
97    )?;
98    Ok(hex(&hash))
99}
100
101/// Write a sibling list bottom-up (§4.1 "Writing a tree"): for each block
102/// its raw blob, its trivia blob when non-empty, its children's node, then
103/// the entry; finally this node. Returns the node's hex hash.
104pub fn write_block_tree(conn: &Connection, blocks: &[TreeInputBlock]) -> Result<String> {
105    let mut entries = Vec::with_capacity(blocks.len());
106    for b in blocks {
107        let raw_hash_hex = put_blob(conn, &b.raw)?;
108        let trivia_hash_hex = if b.trivia.is_empty() {
109            None
110        } else {
111            Some(put_blob(conn, &b.trivia)?)
112        };
113        let child_tree_hash_hex = if b.children.is_empty() {
114            None
115        } else {
116            Some(write_block_tree(conn, &b.children)?)
117        };
118        entries.push(TreeEntry {
119            block_id: b.block_id.clone(),
120            raw_hash_hex,
121            child_tree_hash_hex,
122            kind: b.kind.clone(),
123            attrs: attrs_to_json(&b.attrs),
124            trivia_hash_hex,
125        });
126    }
127    put_tree_node(conn, &entries)
128}
129
130/// `commits.origin`.
131#[derive(Clone, Copy, Debug, PartialEq, Eq)]
132pub enum Origin {
133    Api,
134    Observed,
135    Import,
136    Projection,
137}
138
139impl Origin {
140    #[must_use]
141    pub const fn as_str(&self) -> &'static str {
142        match self {
143            Origin::Api => "api",
144            Origin::Observed => "observed",
145            Origin::Import => "import",
146            Origin::Projection => "projection",
147        }
148    }
149}
150
151/// What a commit row records besides its minted id and sequence number.
152#[derive(Clone, Debug, PartialEq, Eq)]
153pub struct NewCommit<'a> {
154    pub repo_id: &'a str,
155    pub ts: &'a str,
156    pub origin: Origin,
157    pub actor: Option<&'a str>,
158    pub reason: Option<&'a str>,
159    pub checkpoint_id: Option<&'a str>,
160    pub ops: Option<&'a str>,
161}
162
163impl<'a> NewCommit<'a> {
164    /// An `observed` commit with every optional column `NULL`.
165    #[must_use]
166    pub fn observed(repo_id: &'a str, ts: &'a str) -> Self {
167        Self {
168            repo_id,
169            ts,
170            origin: Origin::Observed,
171            actor: None,
172            reason: None,
173            checkpoint_id: None,
174            ops: None,
175        }
176    }
177}
178
179/// Append a commit (§5.4 step 5): **mints `c`**, `seq = 1 + max(seq)` over
180/// the repo. Returns `(commit_id, seq)`.
181pub fn new_commit(
182    conn: &Connection,
183    minter: &mut Mint<'_>,
184    input: &NewCommit<'_>,
185) -> Result<(String, i64)> {
186    let commit_id = minter.mint("c")?;
187    let seq: i64 = conn.query_row(
188        "SELECT COALESCE(MAX(seq), 0) + 1 FROM commits WHERE repo_id = ?1",
189        params![input.repo_id],
190        |r| r.get(0),
191    )?;
192    conn.execute(
193        "INSERT INTO commits (commit_id, repo_id, seq, ts, origin, actor, reason, checkpoint_id, ops)
194         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
195        params![
196            commit_id,
197            input.repo_id,
198            seq,
199            input.ts,
200            input.origin.as_str(),
201            input.actor,
202            input.reason,
203            input.checkpoint_id,
204            input.ops,
205        ],
206    )?;
207    Ok((commit_id, seq))
208}
209
210/// What a revision row records besides its minted id and sequence number.
211#[derive(Clone, Debug, PartialEq, Eq)]
212pub struct NewRevision<'a> {
213    pub doc_id: &'a str,
214    pub root_tree_hex: &'a str,
215    pub frontmatter_blob_hex: Option<&'a str>,
216    pub rendered_hash: [u8; 32],
217    pub path: &'a str,
218    pub commit_id: &'a str,
219}
220
221/// Append a revision (§5.4 step 6): **mints `r`**, `seq = 1 + max(seq)` over
222/// the document. Returns `(rev_id, seq)`.
223pub fn write_revision(
224    conn: &Connection,
225    minter: &mut Mint<'_>,
226    input: &NewRevision<'_>,
227) -> Result<(String, i64)> {
228    let rev_id = minter.mint("r")?;
229    let seq: i64 = conn.query_row(
230        "SELECT COALESCE(MAX(seq), 0) + 1 FROM revisions WHERE doc_id = ?1",
231        params![input.doc_id],
232        |r| r.get(0),
233    )?;
234    let root_tree = from_hex(input.root_tree_hex)?;
235    let frontmatter_blob = input.frontmatter_blob_hex.map(from_hex).transpose()?;
236    conn.execute(
237        "INSERT INTO revisions (rev_id, doc_id, seq, root_tree, frontmatter_blob, rendered_hash, path, commit_id)
238         VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
239        params![
240            rev_id,
241            input.doc_id,
242            seq,
243            root_tree,
244            frontmatter_blob,
245            &input.rendered_hash[..],
246            input.path,
247            input.commit_id,
248        ],
249    )?;
250    Ok((rev_id, seq))
251}