1use 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#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct TreeInputBlock {
20 pub block_id: String,
21 pub kind: String,
23 pub raw: String,
24 pub text: String,
26 pub trivia: String,
28 pub attrs: Attrs,
29 pub span: Span,
33 pub children: Vec<TreeInputBlock>,
34}
35
36pub 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
74pub fn assign_fresh_ids(blocks: &[Block], minter: &mut Mint<'_>) -> Result<Vec<TreeInputBlock>> {
76 assign_from_map(blocks, &BTreeMap::new(), minter)
77}
78
79pub 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
90pub 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
101pub 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#[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#[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 #[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
179pub 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#[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
221pub 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}