Skip to main content

dreamwell_chronicle/
lib.rs

1// dreamwell-chronicle — DuckDB Chronoshift historical warehouse (v1.0.0 LTS).
2//
3// Local OLAP session storage for the Dreamwell editor. Provides queryable
4// rewind index, branch-aware navigation, nearest-checkpoint lookup, and
5// Parquet export. DuckDB NEVER sits in the render loop or frame hot path.
6//
7// Architecture:
8//   SpacetimeDB (live canon) → Editor Session → DreamMemory → Weaver Worker → DuckDB
9//   chronoshift.duckdb → ChronicleReader → Rewind / Export / Analytics
10//
11// Two staging paths:
12//   StagingBuffer: Inline BLAKE3 (55ns/tick). Simple, single-threaded, correct.
13//   DreamMemory:   Deferred BLAKE3 (5ns/tick). Background worker, hardware floor.
14//
15// Schema: 8 tables (sessions, branches, ticks, snapshots, events, roots,
16//         proofs, replay_checks) + _chronicle_meta versioning.
17//
18// Stability: v1.0.0 LTS. No breaking API changes until v2.0.0.
19
20pub 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/// Errors from the chronicle system.
43#[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
76/// Open or create a Chronoshift DuckDB database at the given path.
77/// Runs schema migration if needed. Returns a connection ready for use.
78pub 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
85/// Open an in-memory Chronoshift database (for testing and benchmarks).
86pub 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
93/// Configure DuckDB connection with memory limits and performance settings.
94fn configure_connection(conn: &duckdb::Connection) -> Result<(), ChronicleError> {
95    // Limit memory usage to avoid runaway allocation.
96    conn.execute_batch("SET memory_limit = '256MB';")?;
97    // Use WAL for crash recovery.
98    conn.execute_batch("PRAGMA enable_object_cache;")?;
99    Ok(())
100}
101
102/// Create a ChronicleWriter for a new session on the given connection.
103pub 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
116/// Create a ChronicleReader on the given connection.
117pub 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        // Re-use the connection as a reader.
181        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}