nornir 0.1.0

Companion to cargo: dependency tracking, release gating, deploy, benchmarks, and documentation assembly. Project-agnostic.
Documentation
//! Warehouse — append-only columnar store backing nornir's bench/symbol
//! history.
//!
//! Two impls live behind the [`Warehouse`] trait:
//!
//! - [`local::LocalWarehouse`] writes Parquet files directly to
//!   `<workspace>/.nornir/warehouse/` with an embedded SQLite metadata
//!   catalog. No services, single static binary, default mode.
//! - `RemoteWarehouse` (Phase 5) will talk Apache Arrow Flight to
//!   `nornir-server` and reuse the same trait surface.
//!
//! Both impls follow Iceberg-style partition naming
//! (`repo=…/machine=…/yyyy-mm/`) so a future swap to `iceberg-rust`'s
//! `SqlCatalog` finds the files where it expects them. The trait is the
//! durable abstraction; what's behind it can evolve without callers
//! noticing.

pub mod local;
pub mod schema;
pub mod iceberg;
pub mod iceberg_schema;
pub mod dep_graph;
pub mod funnel;

use std::path::Path;

use anyhow::Result;
use uuid::Uuid;

use crate::bench::BenchRun;
use crate::config::Storage;

/// Storage backend for bench/symbol history. Sync API; the async
/// Flight server wraps a sync impl when it ships.
pub trait Warehouse: Send + Sync {
    fn append_bench_run(&self, repo: &str, run: &BenchRun) -> Result<Uuid>;
    fn query_bench_runs(&self, filter: &BenchFilter) -> Result<Vec<BenchRun>>;
}

#[derive(Debug, Default, Clone)]
pub struct BenchFilter {
    pub repo: Option<String>,
    pub machine: Option<String>,
    pub limit: Option<usize>,
}

impl BenchFilter {
    pub fn for_repo(repo: impl Into<String>) -> Self {
        Self { repo: Some(repo.into()), machine: None, limit: None }
    }
}

/// Open the configured warehouse. Returns a boxed trait object so
/// callers don't bake an impl into their signatures.
pub fn open(storage: &Storage, workspace_root: &Path) -> Result<Box<dyn Warehouse>> {
    match storage.kind.as_str() {
        "" | "local" => {
            let root = if storage.local_path.is_empty() {
                workspace_root.join("workspace_holger/.nornir/warehouse")
            } else {
                workspace_root.join(&storage.local_path).join("warehouse")
            };
            Ok(Box::new(local::LocalWarehouse::open(&root)?))
        }
        "iceberg" => {
            let root = if storage.local_path.is_empty() {
                workspace_root.join("workspace_holger/.nornir/warehouse")
            } else {
                workspace_root.join(&storage.local_path).join("warehouse")
            };
            Ok(Box::new(iceberg::IcebergWarehouse::open(&root)?))
        }
        "remote" => anyhow::bail!("remote warehouse not yet implemented (Phase 5)"),
        other => anyhow::bail!("unknown storage.kind: {other}"),
    }
}