csm-duckdb 0.3.8

Optional analytics, Parquet export, and SQL inspection for chaotic_semantic_memory
use crate::Analytics;
use crate::error::Result;
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct ConceptSummary {
    pub total_concepts: usize,
    pub total_associations: usize,
    pub namespaces: Vec<String>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct BenchmarkSummary {
    pub total_runs: usize,
    pub avg_p50_us: f64,
    pub suites: Vec<String>,
}

impl Analytics {
    pub fn concept_summary(&self) -> Result<ConceptSummary> {
        let total_concepts = self
            .conn
            .query_row("SELECT count(*) FROM concepts", [], |row| {
                row.get::<_, usize>(0)
            })?;

        let total_associations =
            self.conn
                .query_row("SELECT count(*) FROM associations", [], |row| {
                    row.get::<_, usize>(0)
                })?;

        let mut stmt = self
            .conn
            .prepare("SELECT DISTINCT namespace FROM concepts")?;
        let namespaces = stmt
            .query_map([], |row| row.get::<_, String>(0))?
            .collect::<std::result::Result<Vec<_>, _>>()?;

        Ok(ConceptSummary {
            total_concepts,
            total_associations,
            namespaces,
        })
    }

    pub fn benchmark_summary(&self) -> Result<BenchmarkSummary> {
        let total_runs = self
            .conn
            .query_row("SELECT count(*) FROM benchmarks", [], |row| {
                row.get::<_, usize>(0)
            })?;

        let avg_p50_us = self.conn.query_row(
            "SELECT COALESCE(avg(p50_us), 0.0) FROM benchmarks",
            [],
            |row| row.get::<_, f64>(0),
        )?;

        let mut stmt = self.conn.prepare("SELECT DISTINCT suite FROM benchmarks")?;
        let suites = stmt
            .query_map([], |row| row.get::<_, String>(0))?
            .collect::<std::result::Result<Vec<_>, _>>()?;

        Ok(BenchmarkSummary {
            total_runs,
            avg_p50_us,
            suites,
        })
    }
}

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

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

        let c_summary = analytics.concept_summary().unwrap();
        assert_eq!(c_summary.total_concepts, 0);

        let b_summary = analytics.benchmark_summary().unwrap();
        assert_eq!(b_summary.total_runs, 0);
    }
}