Skip to main content

opys_engine/store/
reconstruct.rs

1//! Rows → Doc. Rebuilds the exact `Frontmatter` mapping from the partitioned
2//! tables (columns + tags + relations + fm_fields); the canonical serializer
3//! makes key insertion order irrelevant, so the merge is order-free. The gate
4//! invariant is `reconstruct(decompose(doc)).to_text() == doc.to_text()`.
5
6use std::collections::BTreeMap;
7
8use serde_norway::{Mapping, Value as Yaml};
9
10use crate::body;
11use crate::doc::Doc;
12use crate::error::{OpysError, Result};
13use crate::frontmatter::Frontmatter;
14
15use super::{g_i64, g_str, IntoParam, Store};
16
17/// One fully-reconstructed row of `docs` plus its load snapshot (flush input).
18pub(crate) struct FullRow {
19    pub dkey: i64,
20    pub relpath: String,
21    pub orig_relpath: Option<String>,
22    pub orig_text: Option<String>,
23    #[allow(dead_code)] // sync's mtime backfill (engine flip step)
24    pub orig_mtime: Option<String>,
25    pub doc: Doc,
26}
27
28/// Partial frontmatter parts collected per dkey before assembly.
29#[derive(Default)]
30struct Parts {
31    tags: Vec<(i64, String)>,
32    relations: Vec<(String, i64, String, String)>, // (field, seq, ref_id, raw_value)
33    fm_fields: Vec<(String, String)>,              // (key, value_yaml)
34}
35
36impl Store {
37    /// Reconstruct a single document.
38    pub fn doc(&mut self, dkey: i64) -> Result<Doc> {
39        let (_, rows) = self.select(
40            "SELECT dkey, id, status, created, updated, body, path FROM docs WHERE dkey = $1",
41            vec![dkey.into_param()],
42        )?;
43        let row = rows
44            .into_iter()
45            .next()
46            .ok_or_else(|| OpysError::Store(format!("no docs row for dkey {dkey}")))?;
47        let parts = self
48            .parts_for(Some(dkey))?
49            .remove(&dkey)
50            .unwrap_or_default();
51        self.assemble(&row, parts).map(|f| f.doc)
52    }
53
54    /// Reconstruct every document, in dkey (load/path) order.
55    pub fn all_docs(&mut self) -> Result<Vec<(i64, Doc)>> {
56        Ok(self
57            .full_rows()?
58            .into_iter()
59            .map(|f| (f.dkey, f.doc))
60            .collect())
61    }
62
63    /// Every doc with its snapshot columns — the flush working set.
64    pub(crate) fn full_rows(&mut self) -> Result<Vec<FullRow>> {
65        let (_, rows) = self.select(
66            "SELECT dkey, id, status, created, updated, body, path, \
67             orig_path, orig_text, orig_mtime FROM docs ORDER BY dkey",
68            vec![],
69        )?;
70        let mut parts = self.parts_for(None)?;
71        rows.into_iter()
72            .map(|row| {
73                let dkey = g_i64(&row[0])
74                    .ok_or_else(|| OpysError::Store("docs.dkey is not an integer".into()))?;
75                let p = parts.remove(&dkey).unwrap_or_default();
76                self.assemble(&row, p)
77            })
78            .collect()
79    }
80
81    /// Child-table parts, for one dkey or all.
82    fn parts_for(&mut self, dkey: Option<i64>) -> Result<BTreeMap<i64, Parts>> {
83        let mut out: BTreeMap<i64, Parts> = BTreeMap::new();
84        let (filter, params) = match dkey {
85            Some(k) => (" WHERE dkey = $1", vec![k.into_param()]),
86            None => ("", vec![]),
87        };
88
89        let (_, rows) = self.select(
90            &format!("SELECT dkey, seq, tag FROM tags{filter} ORDER BY dkey, seq"),
91            params.clone(),
92        )?;
93        for r in rows {
94            let (k, seq) = (need_i64(&r[0])?, need_i64(&r[1])?);
95            out.entry(k).or_default().tags.push((seq, need_str(&r[2])?));
96        }
97
98        let (_, rows) = self.select(
99            &format!(
100                "SELECT dkey, field, seq, ref_id, raw_value FROM relations{filter} \
101                 ORDER BY dkey, seq"
102            ),
103            params.clone(),
104        )?;
105        for r in rows {
106            let k = need_i64(&r[0])?;
107            out.entry(k).or_default().relations.push((
108                need_str(&r[1])?,
109                need_i64(&r[2])?,
110                need_str(&r[3])?,
111                need_str(&r[4])?,
112            ));
113        }
114
115        // Element rows (value_yaml NULL) are query-only; only fidelity rows
116        // participate in reconstruction.
117        let not_elem = if filter.is_empty() {
118            " WHERE value_yaml IS NOT NULL"
119        } else {
120            " AND value_yaml IS NOT NULL"
121        };
122        let (_, rows) = self.select(
123            &format!("SELECT dkey, key, value_yaml FROM fm_fields{filter}{not_elem} ORDER BY dkey"),
124            params,
125        )?;
126        for r in rows {
127            let k = need_i64(&r[0])?;
128            out.entry(k)
129                .or_default()
130                .fm_fields
131                .push((need_str(&r[1])?, need_str(&r[2])?));
132        }
133        Ok(out)
134    }
135
136    /// Assemble a docs row (dkey, id, status, created, updated, body, path,
137    /// [orig_path, orig_text, orig_mtime]) + child parts into a `FullRow`.
138    fn assemble(&self, row: &[gluesql::prelude::Value], parts: Parts) -> Result<FullRow> {
139        let dkey =
140            g_i64(&row[0]).ok_or_else(|| OpysError::Store("docs.dkey not integer".into()))?;
141        let mut map = Mapping::new();
142        for (i, key) in [(1, "id"), (2, "status"), (3, "created"), (4, "updated")] {
143            if let Some(s) = g_str(&row[i]) {
144                map.insert(Yaml::String(key.to_string()), Yaml::String(s));
145            }
146        }
147        if !parts.tags.is_empty() {
148            let mut tags = parts.tags;
149            tags.sort_by_key(|(seq, _)| *seq);
150            let seq = tags.into_iter().map(|(_, t)| Yaml::String(t)).collect();
151            map.insert(Yaml::String("tags".to_string()), Yaml::Sequence(seq));
152        }
153        for field in crate::refs::RELATION_FIELDS {
154            let mut entries: Vec<_> = parts.relations.iter().filter(|e| e.0 == field).collect();
155            if entries.is_empty() {
156                continue;
157            }
158            entries.sort_by_key(|e| e.1);
159            let mut m = Mapping::new();
160            for e in entries {
161                m.insert(Yaml::String(e.2.clone()), Yaml::String(e.3.clone()));
162            }
163            map.insert(Yaml::String(field.to_string()), Yaml::Mapping(m));
164        }
165        for (key, value_yaml) in parts.fm_fields {
166            let value: Yaml = serde_norway::from_str(&value_yaml)
167                .map_err(|e| OpysError::Store(format!("cannot parse stored field '{key}': {e}")))?;
168            map.insert(Yaml::String(key), value);
169        }
170
171        let body = need_str(&row[5])?;
172        let relpath = need_str(&row[6])?;
173        let title = body::title(&body);
174        Ok(FullRow {
175            dkey,
176            orig_relpath: row.get(7).and_then(g_str),
177            orig_text: row.get(8).and_then(g_str),
178            orig_mtime: row.get(9).and_then(g_str),
179            doc: Doc {
180                path: self.abspath(&relpath),
181                frontmatter: Frontmatter { map },
182                body,
183                title,
184            },
185            relpath,
186        })
187    }
188}
189
190fn need_str(v: &gluesql::prelude::Value) -> Result<String> {
191    g_str(v).ok_or_else(|| OpysError::Store(format!("expected TEXT value, got {v:?}")))
192}
193
194fn need_i64(v: &gluesql::prelude::Value) -> Result<i64> {
195    g_i64(v).ok_or_else(|| OpysError::Store(format!("expected INTEGER value, got {v:?}")))
196}