Skip to main content

structscope_provenance/
lib.rs

1use anyhow::Result;
2use rusqlite::{params, Connection};
3use serde::{Deserialize, Serialize};
4use serde_json::json;
5use std::fs;
6use std::fs::OpenOptions;
7use std::io::Write;
8use std::path::{Path, PathBuf};
9use structscope_events::Event;
10use uuid::Uuid;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct ProvenanceConfig {
14    pub sqlite_path: Option<PathBuf>,
15    pub jsonl_path: Option<PathBuf>,
16}
17
18#[derive(Debug)]
19pub struct ProvenanceRecorder {
20    run_id: String,
21    sqlite: Option<Connection>,
22    jsonl_path: Option<PathBuf>,
23}
24
25impl ProvenanceRecorder {
26    pub fn open(config: &ProvenanceConfig, command: &str) -> Result<Self> {
27        let run_id = Uuid::new_v4().to_string();
28        let sqlite = match &config.sqlite_path {
29            Some(path) => {
30                ensure_parent_dir(path)?;
31                let conn = Connection::open(path)?;
32                init_schema(&conn)?;
33                conn.execute(
34                    "INSERT INTO runs (run_id, command, started_at) VALUES (?1, ?2, datetime('now'))",
35                    params![run_id, command],
36                )?;
37                Some(conn)
38            }
39            None => None,
40        };
41
42        Ok(Self {
43            run_id,
44            sqlite,
45            jsonl_path: config.jsonl_path.clone(),
46        })
47    }
48
49    pub fn run_id(&self) -> &str {
50        &self.run_id
51    }
52
53    pub fn record(&mut self, event: Event) -> Result<()> {
54        if let Some(conn) = &self.sqlite {
55            conn.execute(
56                "INSERT INTO events (run_id, event, timestamp, structure_id, details_json) VALUES (?1, ?2, ?3, ?4, ?5)",
57                params![
58                    &self.run_id,
59                    &event.event,
60                    event.timestamp.to_rfc3339(),
61                    &event.structure_id,
62                    event.details.to_string()
63                ],
64            )?;
65        }
66
67        if let Some(path) = &self.jsonl_path {
68            append_jsonl(path, &event)?;
69        }
70
71        Ok(())
72    }
73
74    pub fn finish(&mut self) -> Result<()> {
75        self.record(Event::new("run_complete", None, json!({ "run_id": &self.run_id })))
76    }
77}
78
79pub fn inspect_sqlite(path: &Path) -> Result<Vec<String>> {
80    let conn = Connection::open(path)?;
81    let mut stmt = conn.prepare(
82        "SELECT run_id, command, started_at FROM runs ORDER BY started_at DESC",
83    )?;
84    let rows = stmt.query_map([], |row| {
85        Ok(format!(
86            "{}\t{}\t{}",
87            row.get::<_, String>(0)?,
88            row.get::<_, String>(1)?,
89            row.get::<_, String>(2)?
90        ))
91    })?;
92
93    let mut out = Vec::new();
94    for row in rows {
95        out.push(row?);
96    }
97    Ok(out)
98}
99
100fn init_schema(conn: &Connection) -> Result<()> {
101    conn.execute_batch(
102        "
103        CREATE TABLE IF NOT EXISTS runs (
104            run_id TEXT PRIMARY KEY,
105            command TEXT NOT NULL,
106            started_at TEXT NOT NULL
107        );
108        CREATE TABLE IF NOT EXISTS events (
109            id INTEGER PRIMARY KEY AUTOINCREMENT,
110            run_id TEXT NOT NULL,
111            event TEXT NOT NULL,
112            timestamp TEXT NOT NULL,
113            structure_id TEXT,
114            details_json TEXT NOT NULL
115        );
116        ",
117    )?;
118    Ok(())
119}
120
121fn append_jsonl(path: &Path, event: &Event) -> Result<()> {
122    ensure_parent_dir(path)?;
123    let mut file = OpenOptions::new().create(true).append(true).open(path)?;
124    writeln!(file, "{}", serde_json::to_string(event)?)?;
125    Ok(())
126}
127
128fn ensure_parent_dir(path: &Path) -> Result<()> {
129    if let Some(parent) = path.parent() {
130        if !parent.as_os_str().is_empty() {
131            fs::create_dir_all(parent)?;
132        }
133    }
134    Ok(())
135}