csm-duckdb 0.3.8

Optional analytics, Parquet export, and SQL inspection for chaotic_semantic_memory
use crate::Analytics;
use crate::error::Result;
use crate::ingest_export::IngestReport;
use std::path::Path;

impl Analytics {
    pub fn load_benchmarks_dir<P: AsRef<Path>>(&mut self, dir: P) -> Result<IngestReport> {
        let mut benchmarks_loaded = 0;

        if !dir.as_ref().exists() {
            return Ok(IngestReport::default());
        }

        let tx = self.conn.transaction()?;
        for entry in std::fs::read_dir(dir)? {
            let entry = entry?;
            let path = entry.path();
            if path.extension().is_some_and(|ext| ext == "jsonl") {
                let file = std::fs::read_to_string(&path)?;
                for line in file.lines() {
                    if line.trim().is_empty() {
                        continue;
                    }
                    let res: serde_json::Value = serde_json::from_str(line)?;

                    let suite = path
                        .file_stem()
                        .and_then(|s| s.to_str())
                        .unwrap_or("unknown");
                    let name = res["query_id"].as_str().ok_or_else(|| {
                        crate::error::AnalyticsError::InvalidInput("Missing query_id".to_string())
                    })?;
                    let p50_us = res["latency_ms"].as_f64().ok_or_else(|| {
                        crate::error::AnalyticsError::InvalidInput(
                            "Missing or invalid latency_ms".to_string(),
                        )
                    })? * 1000.0;
                    let extras = serde_json::to_string(&res)?;

                    tx.execute(
                        "INSERT INTO benchmarks (suite, name, run_at_us, p50_us, extras)
                         VALUES (?, ?, ?, ?, ?)",
                        duckdb::params![suite, name, 0, p50_us, extras],
                    )?;
                    benchmarks_loaded += 1;
                }
            }
        }
        tx.commit()?;

        Ok(IngestReport {
            benchmarks_loaded,
            ..Default::default()
        })
    }
}

#[cfg(test)]
mod tests {
    #![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
    use super::*;
    use crate::schema::SCHEMA_DDL;
    use duckdb::Connection;
    use std::io::Write;

    #[test]
    fn test_load_benchmarks_minimal() {
        let conn = Connection::open_in_memory().unwrap();
        conn.execute_batch(SCHEMA_DDL).unwrap();
        let mut analytics = Analytics { conn };

        let dir = ::tempfile::tempdir().unwrap();
        let file_path = dir.path().join("test.jsonl");
        let mut file = std::fs::File::create(file_path).unwrap();
        writeln!(file, r#"{{"query_id": "b1", "latency_ms": 1.0}}"#).unwrap();

        let report = analytics.load_benchmarks_dir(dir.path()).unwrap();
        assert_eq!(report.benchmarks_loaded, 1);
    }

    #[test]
    fn test_load_benchmarks_invalid() {
        let conn = Connection::open_in_memory().unwrap();
        conn.execute_batch(SCHEMA_DDL).unwrap();
        let mut analytics = Analytics { conn };

        let dir = ::tempfile::tempdir().unwrap();
        let file_path = dir.path().join("invalid.jsonl");
        let mut file = std::fs::File::create(file_path).unwrap();
        // Missing latency_ms
        writeln!(file, r#"{{"query_id": "b1"}}"#).unwrap();

        let res = analytics.load_benchmarks_dir(dir.path());
        assert!(res.is_err());
        assert!(format!("{res:?}").contains("Missing or invalid latency_ms"));
    }
}