opys_engine/store/flush.rs
1//! Persisting the store back to the durable medium is split in two: this module
2//! computes a **medium-agnostic** [`FlushPlan`] from the store's rows and load
3//! snapshot (no filesystem access), and the storage backend executes it. The
4//! plan makes the medium match the tables: docs deleted from the store lose
5//! their files, docs whose `path` moved are renamed, and a doc's file is
6//! (re)written only when its reconstructed canonical text differs from the text
7//! rendered at load. Ordering on apply is deletes → renames → writes, so a
8//! rename never lands on a path a delete is about to vacate. Finally
9//! `_retired.md` is (re)written iff an id was reserved this run, and any legacy
10//! `_retired.txt` is migrated to it.
11//!
12//! The `opys-backend-markdown-local` crate holds the local-filesystem executor
13//! (`apply_plan`) and the load-side walk/parse; a backend for another medium
14//! would execute the same plan differently.
15
16use std::collections::HashSet;
17use std::path::PathBuf;
18
19use crate::error::Result;
20use crate::project::Project;
21
22use super::{g_i64, g_str, Store};
23
24/// What a flush must persist, as absolute paths and rendered text — computed
25/// without touching the filesystem so any backend can execute it.
26#[derive(Debug, Default)]
27pub struct FlushPlan {
28 /// Document files to remove (a doc was deleted/closed/retired).
29 pub deletes: Vec<PathBuf>,
30 /// Document files to move (`from`, `to`) after a layout/status change.
31 pub renames: Vec<(PathBuf, PathBuf)>,
32 /// Document files to (re)write (`path`, canonical text).
33 pub writes: Vec<(PathBuf, String)>,
34 /// The retired ledger to (re)write (`_retired.md` path, serialized text).
35 pub retired_write: Option<(PathBuf, String)>,
36 /// A legacy `_retired.txt` to delete (migrated to `_retired.md`).
37 pub legacy_remove: Option<PathBuf>,
38}
39
40impl Store {
41 /// Compute the [`FlushPlan`] — no filesystem access. Consumes the store, the
42 /// final read of an invocation.
43 pub fn flush_plan(mut self, prj: &Project) -> Result<FlushPlan> {
44 let rows = self.full_rows()?;
45 let mut plan = FlushPlan::default();
46
47 // 1. Deletions: loaded rows that no longer exist in `docs`.
48 let surviving: HashSet<i64> = rows.iter().map(|r| r.dkey).collect();
49 for (dkey, load_path) in &self.loaded {
50 if !surviving.contains(dkey) {
51 plan.deletes.push(load_path.clone());
52 }
53 }
54
55 // 2. Renames: the authoritative path moved away from the load path.
56 for r in &rows {
57 if let Some(orig) = &r.orig_relpath {
58 if *orig != r.relpath {
59 plan.renames
60 .push((self.abspath(orig), self.abspath(&r.relpath)));
61 }
62 }
63 }
64
65 // 3. Writes: new docs always; existing docs only on logical change.
66 for r in &rows {
67 let text = r.doc.to_text();
68 let write = match &r.orig_text {
69 None => true,
70 Some(orig) => *orig != text,
71 };
72 if write {
73 plan.writes.push((self.abspath(&r.relpath), text));
74 }
75 }
76
77 // 4. Retired ledger: (re)write `_retired.md` when an id was reserved
78 // this run or a legacy `_retired.txt` is present (migration). The
79 // legacy-present flag was captured at load, so this stays fs-free.
80 let (_, retired) = self.select(
81 "SELECT rkey, id, num, title FROM retired ORDER BY rkey",
82 vec![],
83 )?;
84 let grew = retired.len() > self.retired_loaded;
85 if grew || self.retired_legacy {
86 let mut entries: Vec<(u64, (String, String))> = retired
87 .iter()
88 .map(|r| {
89 let num = g_i64(&r[2]).map(|n| n as u64).unwrap_or(u64::MAX);
90 let id = g_str(&r[1]).unwrap_or_default();
91 let title = g_str(&r[3]).unwrap_or_default();
92 (num, (id, title))
93 })
94 .collect();
95 entries.sort_by_key(|e| e.0); // stable: ties keep rkey order
96 let pairs: Vec<(String, String)> = entries.into_iter().map(|e| e.1).collect();
97 if !pairs.is_empty() {
98 plan.retired_write = Some((
99 crate::retired::path(&prj.base),
100 crate::retired::serialize(&pairs),
101 ));
102 }
103 if self.retired_legacy {
104 plan.legacy_remove = Some(crate::retired::legacy_path(&prj.base));
105 }
106 }
107 Ok(plan)
108 }
109}