From 9692025790edabff11f905448ed8dbdacca232b3 Mon Sep 17 00:00:00 2001 From: anthonyrawlins Date: Tue, 3 Mar 2026 17:24:05 +1100 Subject: [PATCH] Implement chrs-graph: Dolt-backed versioned state management --- chrs-graph/Cargo.toml | 14 ++++++ chrs-graph/src/lib.rs | 110 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 chrs-graph/Cargo.toml create mode 100644 chrs-graph/src/lib.rs diff --git a/chrs-graph/Cargo.toml b/chrs-graph/Cargo.toml new file mode 100644 index 00000000..09a5a600 --- /dev/null +++ b/chrs-graph/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "chrs-graph" +version = "0.1.0" +edition = "2024" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "1.0" +chrono = { version = "0.4", features = ["serde"] } +uuid = { version = "1", features = ["serde", "v4"] } + +[dev-dependencies] +tempfile = "3" diff --git a/chrs-graph/src/lib.rs b/chrs-graph/src/lib.rs new file mode 100644 index 00000000..44224c9b --- /dev/null +++ b/chrs-graph/src/lib.rs @@ -0,0 +1,110 @@ +use chrono::Utc; +use serde_json::Value; +use std::{path::Path, process::Command}; +use thiserror::Error; +use uuid::Uuid; + +#[derive(Error, Debug)] +pub enum GraphError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + #[error("Command failed: {0}")] + CommandFailed(String), + #[error("Serde JSON error: {0}")] + SerdeJson(#[from] serde_json::Error), + #[error("Other error: {0}")] + Other(String), +} + +pub struct DoltGraph { + repo_path: std::path::PathBuf, +} + +impl DoltGraph { + pub fn init(path: &Path) -> Result { + let status = Command::new("dolt") + .arg("init") + .current_dir(path) + .status()?; + if !status.success() { + return Err(GraphError::CommandFailed(format!( + "dolt init failed with status {:?}", + status + ))); + } + Ok(Self { + repo_path: path.to_path_buf(), + }) + } + + fn run_cmd(&self, args: &[&str]) -> Result<(), GraphError> { + let output = Command::new("dolt") + .args(args) + .current_dir(&self.repo_path) + .output()?; + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(GraphError::CommandFailed(stderr.to_string())); + } + Ok(()) + } + + pub fn commit(&self, message: &str) -> Result<(), GraphError> { + self.run_cmd(&["add", "."])?; + self.run_cmd(&["commit", "-m", message])?; + Ok(()) + } + + pub fn create_table(&self, table_name: &str, schema: &str) -> Result<(), GraphError> { + let query = format!("CREATE TABLE {} ({})", table_name, schema); + self.run_cmd(&["sql", "-q", &query])?; + Ok(()) + } + + pub fn insert_node(&self, table: &str, data: Value) -> Result<(), GraphError> { + let obj = data + .as_object() + .ok_or_else(|| GraphError::Other("Data must be a JSON object".into()))?; + let columns: Vec = obj.keys().cloned().collect(); + let mut values: Vec = Vec::new(); + for key in &columns { + let v = &obj[key]; + let sql_val = match v { + Value::String(s) => format!("'{}'", s.replace('\'', "''")), + Value::Number(n) => n.to_string(), + Value::Bool(b) => { + if *b { + "TRUE".into() + } else { + "FALSE".into() + } + } + Value::Null => "NULL".into(), + _ => return Err(GraphError::Other("Unsupported JSON value type".into())), + }; + values.push(sql_val); + } + let query = format!( + "INSERT INTO {} ({}) VALUES ({})", + table, + columns.join(", "), + values.join(", ") + ); + self.run_cmd(&["sql", "-q", &query])?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + #[test] + fn test_init_create_table_and_commit() { + let dir = TempDir::new().unwrap(); + let graph = DoltGraph::init(dir.path()).expect("init failed"); + graph.create_table("nodes", "id INT PRIMARY KEY, name TEXT").expect("create table failed"); + graph.commit("initial commit with table").expect("commit failed"); + } +}