Implement chrs-graph: Dolt-backed versioned state management

This commit is contained in:
anthonyrawlins
2026-03-03 17:24:05 +11:00
parent b3c37825b2
commit 9692025790
2 changed files with 124 additions and 0 deletions

14
chrs-graph/Cargo.toml Normal file
View File

@@ -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"

110
chrs-graph/src/lib.rs Normal file
View File

@@ -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<Self, GraphError> {
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<String> = obj.keys().cloned().collect();
let mut values: Vec<String> = 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");
}
}