Skip to main content

sqlite_graphrag/
atomic_io.rs

1//! Atomic file writes following the atomwrite algorithm (rules_rust_escrita_atomica).
2//!
3//! Sequence: tempfile in the **same** directory → write → fsync → rename →
4//! fsync parent directory. Readers never observe a partial file.
5
6use crate::errors::AppError;
7use std::io::Write;
8use std::path::Path;
9
10/// Atomically write `bytes` to `path` (create or overwrite).
11///
12/// Mirrors the atomwrite CLI contract used by LLM agents: crash-safe,
13/// same-filesystem rename, parent-dir fsync on Unix.
14///
15/// # Errors
16/// Returns [`AppError::Io`] when any step of the write/rename sequence fails.
17/// Returns [`AppError::Validation`] when `path` has no parent directory component.
18pub fn write_atomic(path: &Path, bytes: &[u8]) -> Result<(), AppError> {
19    let parent = path.parent().filter(|p| !p.as_os_str().is_empty());
20    let dir = match parent {
21        Some(p) => {
22            std::fs::create_dir_all(p)?;
23            p
24        }
25        None => Path::new("."),
26    };
27
28    let mut tmp = tempfile::NamedTempFile::new_in(dir).map_err(AppError::Io)?;
29    tmp.write_all(bytes).map_err(AppError::Io)?;
30    tmp.as_file().sync_all().map_err(AppError::Io)?;
31    tmp.persist(path).map_err(|e| {
32        AppError::Io(std::io::Error::other(format!(
33            "atomic persist failed for {}: {e}",
34            path.display()
35        )))
36    })?;
37
38    #[cfg(unix)]
39    {
40        if let Ok(dir_file) = std::fs::File::open(dir) {
41            let _ = dir_file.sync_all();
42        }
43    }
44
45    Ok(())
46}
47
48/// Serialize `value` as pretty JSON and write it atomically to `path`.
49///
50/// Appends a trailing newline so the file is a complete JSON document suitable
51/// for `jaq` / `jq` without further munging.
52pub fn write_json_atomic<T: serde::Serialize>(path: &Path, value: &T) -> Result<(), AppError> {
53    let mut json = serde_json::to_string_pretty(value)?;
54    json.push('\n');
55    write_atomic(path, json.as_bytes())
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use serde_json::json;
62    use tempfile::TempDir;
63
64    #[test]
65    fn write_atomic_round_trip() {
66        let tmp = TempDir::new().unwrap();
67        let path = tmp.path().join("out.json");
68        write_atomic(&path, b"{\"ok\":true}\n").unwrap();
69        let got = std::fs::read_to_string(&path).unwrap();
70        assert_eq!(got, "{\"ok\":true}\n");
71    }
72
73    #[test]
74    fn write_json_atomic_is_valid_json() {
75        let tmp = TempDir::new().unwrap();
76        let path = tmp.path().join("envelope.json");
77        write_json_atomic(&path, &json!({"a": 1, "b": "x"})).unwrap();
78        let raw = std::fs::read_to_string(&path).unwrap();
79        let v: serde_json::Value = serde_json::from_str(&raw).expect("must parse");
80        assert_eq!(v["a"], 1);
81    }
82
83    #[test]
84    fn write_atomic_creates_parent_dirs() {
85        let tmp = TempDir::new().unwrap();
86        let path = tmp.path().join("nested").join("dir").join("f.txt");
87        write_atomic(&path, b"hi\n").unwrap();
88        assert_eq!(std::fs::read_to_string(&path).unwrap(), "hi\n");
89    }
90}