dreamwell_chronicle/
lib.rs1pub mod chronicler;
21pub mod exporter;
22pub mod pipeline;
23pub mod reader;
24pub mod retention;
25pub mod schema;
26pub mod writer;
27
28use std::path::Path;
29
30pub use chronicler::{
31 parse_dql, ChronicleReceipt, ChroniclerClient, ChroniclerService, DqlCommand, ProofResult, QueryResult,
32 RewindResult,
33};
34pub use pipeline::{
35 compute_attestation, compute_attestation_parallel, event_kind_index, grade_index, Braid, ChroniclePipeline,
36 DeferredEvent, DreamMemory, StagedEvent, StagedTick, StagingBuffer, EVENT_KIND_TABLE, GRADE_TABLE,
37};
38pub use reader::ChronicleReader;
39pub use retention::{ArchiveFormat, RetentionPolicy};
40pub use writer::{BranchRow, ChronicleWriter, EventRow, RootRow, SessionRow, SnapshotRow, TickRow};
41
42#[derive(Debug)]
44pub enum ChronicleError {
45 Duckdb(duckdb::Error),
46 Io(std::io::Error),
47 Schema(String),
48 InvalidBranchId(String),
49}
50
51impl std::fmt::Display for ChronicleError {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 match self {
54 Self::Duckdb(e) => write!(f, "duckdb: {}", e),
55 Self::Io(e) => write!(f, "io: {}", e),
56 Self::Schema(s) => write!(f, "schema: {}", s),
57 Self::InvalidBranchId(id) => write!(f, "invalid branch_id: {}", id),
58 }
59 }
60}
61
62impl std::error::Error for ChronicleError {}
63
64impl From<duckdb::Error> for ChronicleError {
65 fn from(e: duckdb::Error) -> Self {
66 Self::Duckdb(e)
67 }
68}
69
70impl From<std::io::Error> for ChronicleError {
71 fn from(e: std::io::Error) -> Self {
72 Self::Io(e)
73 }
74}
75
76pub fn open_chronicle(path: &Path) -> Result<duckdb::Connection, ChronicleError> {
79 let conn = duckdb::Connection::open(path)?;
80 configure_connection(&conn)?;
81 schema::create_schema(&conn)?;
82 Ok(conn)
83}
84
85pub fn open_chronicle_in_memory() -> Result<duckdb::Connection, ChronicleError> {
87 let conn = duckdb::Connection::open_in_memory()?;
88 configure_connection(&conn)?;
89 schema::create_schema(&conn)?;
90 Ok(conn)
91}
92
93fn configure_connection(conn: &duckdb::Connection) -> Result<(), ChronicleError> {
95 conn.execute_batch("SET memory_limit = '256MB';")?;
97 conn.execute_batch("PRAGMA enable_object_cache;")?;
99 Ok(())
100}
101
102pub fn new_writer(conn: duckdb::Connection, project_name: &str) -> Result<ChronicleWriter, ChronicleError> {
104 let session_id = uuid::Uuid::new_v4().to_string();
105 let branch_id = "main".to_string();
106 let now_ms = std::time::SystemTime::now()
107 .duration_since(std::time::UNIX_EPOCH)
108 .unwrap_or_default()
109 .as_millis() as i64;
110
111 let mut writer = ChronicleWriter::open(conn, session_id, branch_id);
112 writer.begin_session(project_name, now_ms)?;
113 Ok(writer)
114}
115
116pub fn new_reader(conn: duckdb::Connection) -> ChronicleReader {
118 ChronicleReader::open(conn)
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn open_in_memory_and_write() {
127 let conn = open_chronicle_in_memory().unwrap();
128 let mut writer = new_writer(conn, "test_project").unwrap();
129 writer
130 .append_tick(TickRow {
131 branch_id: writer.branch_id().to_string(),
132 tick: 0,
133 wall_time_us: 16000,
134 cpu_us: 100,
135 gpu_us: 80,
136 fps: 60.0,
137 draw_calls: 10,
138 grade: "A".into(),
139 })
140 .unwrap();
141 writer.flush().unwrap();
142 assert_eq!(writer.total_ticks_flushed(), 1);
143 }
144
145 #[test]
146 fn open_file_backed() {
147 let path = std::env::temp_dir().join("dreamwell_chronicle_test.duckdb");
148 let _ = std::fs::remove_file(&path);
149 let conn = open_chronicle(&path).unwrap();
150 let writer = new_writer(conn, "test").unwrap();
151 assert!(!writer.session_id().is_empty());
152 drop(writer);
153 let _ = std::fs::remove_file(&path);
154 }
155
156 #[test]
157 fn roundtrip_write_read() {
158 let conn = open_chronicle_in_memory().unwrap();
159 let mut writer = new_writer(conn, "roundtrip").unwrap();
160 let branch = writer.branch_id().to_string();
161
162 for i in 0..50 {
163 writer
164 .append_tick(TickRow {
165 branch_id: branch.clone(),
166 tick: i,
167 wall_time_us: 16000 + i,
168 cpu_us: 100,
169 gpu_us: 80,
170 fps: 60.0,
171 draw_calls: 10,
172 grade: "A".into(),
173 })
174 .unwrap();
175 }
176 writer.append_event("test", 25, r#"{"key":"value"}"#).unwrap();
177 writer.flush().unwrap();
178 writer.end_session(99999, "Happy").unwrap();
179
180 let reader = new_reader(writer.into_connection());
182 assert_eq!(reader.count_ticks(&branch).unwrap(), 50);
183 assert_eq!(reader.count_events(&branch).unwrap(), 1);
184
185 let sessions = reader.list_sessions().unwrap();
186 assert_eq!(sessions.len(), 1);
187 assert_eq!(sessions[0].verdict.as_deref(), Some("Happy"));
188 }
189}