1use 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
18pub 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
30pub 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#[derive(Clone, Debug, PartialEq, Eq)]
79pub struct RevisionRead {
80 pub path: String,
82 pub content: String,
83 pub rendered_hash_match: bool,
85}
86
87pub 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
153pub 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 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 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 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
257pub 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}