Implement chrs-graph: Dolt-backed versioned state management
This commit is contained in:
14
chrs-graph/Cargo.toml
Normal file
14
chrs-graph/Cargo.toml
Normal 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
110
chrs-graph/src/lib.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user