nodedb-lite 0.1.0

Embedded edge client for NodeDB — offline vector search, graph traversal, and CRDT sync
Documentation
// SPDX-License-Identifier: Apache-2.0
//! Atomic transaction batch and Calvin deterministic execution for Lite.
//!
//! On Origin these are dispatched by the Multi-Raft sequencer to guarantee
//! deterministic ordering across shards. On Lite there is only one shard and
//! one writer — single-node execution is already deterministic. All three
//! Calvin variants and `TransactionBatch` collapse to the same operation:
//! execute each physical plan in order, short-circuiting on the first error.
//!
//! "Atomicity" on Lite is provided by executing plans sequentially while
//! holding the engine's existing mutexes (CrdtEngine, StrictEngine, etc.).
//! Because there is no concurrent writer, the sequence is equivalent to a
//! single atomic commit.

use nodedb_physical::physical_plan::PhysicalPlan;
use nodedb_types::result::QueryResult;

use crate::error::LiteError;
use crate::query::engine::LiteQueryEngine;
use crate::query::physical_visitor::LiteDataPlaneVisitor;
use crate::storage::engine::StorageEngine;

/// Execute `plans` in order, stopping on the first error.
///
/// This shared helper is used by `TransactionBatch`, `CalvinExecuteStatic`,
/// and `CalvinExecuteActive`. Each plan is dispatched through the full
/// `LiteDataPlaneVisitor` so every engine family is handled correctly.
pub async fn execute_plans_in_order<S: StorageEngine>(
    engine: &LiteQueryEngine<S>,
    plans: &[PhysicalPlan],
) -> Result<QueryResult, LiteError> {
    let mut last = QueryResult::empty();
    for plan in plans {
        let mut visitor = LiteDataPlaneVisitor::new(engine);
        let fut = nodedb_physical::dispatch(&mut visitor, plan)?;
        last = fut.await?;
    }
    Ok(last)
}

/// Handle `MetaOp::TransactionBatch { plans }`.
pub async fn handle_txn_batch<S: StorageEngine>(
    engine: &LiteQueryEngine<S>,
    plans: &[PhysicalPlan],
) -> Result<QueryResult, LiteError> {
    execute_plans_in_order(engine, plans).await
}

/// Handle `MetaOp::CalvinExecuteStatic { plans, .. }`.
///
/// Static-set Calvin means the read/write set was fully known at submission
/// time. On Lite, determinism is guaranteed by single-node serialised
/// execution — no sequencer required.
pub async fn handle_calvin_static<S: StorageEngine>(
    engine: &LiteQueryEngine<S>,
    plans: &[PhysicalPlan],
) -> Result<QueryResult, LiteError> {
    execute_plans_in_order(engine, plans).await
}

/// Handle `MetaOp::CalvinExecutePassive { keys_to_read, .. }`.
///
/// On Origin the passive participant reads keys on a remote vshard and
/// broadcasts the values to active participants so they can write
/// deterministically without a round-trip. On Lite there is exactly one
/// node: the "passive" vshard and the "active" vshard are the same node,
/// so no cross-shard broadcast occurs and the active executor (`CalvinExecuteActive`)
/// will read the local data directly when it runs. Returning a successful
/// empty result here is the correct single-node behaviour — it signals to
/// the caller that the passive phase completed without error.
pub async fn handle_calvin_passive() -> Result<QueryResult, LiteError> {
    Ok(QueryResult::empty())
}

/// Handle `MetaOp::CalvinExecuteActive { plans, .. }`.
///
/// Active Calvin participants execute the write plans after receiving injected
/// read values from passive participants. On Lite, all data is local and
/// `injected_reads` is never populated by a remote shard. We execute the plans
/// directly — the injected reads are ignored because the handlers already read
/// from the local engine.
pub async fn handle_calvin_active<S: StorageEngine>(
    engine: &LiteQueryEngine<S>,
    plans: &[PhysicalPlan],
) -> Result<QueryResult, LiteError> {
    execute_plans_in_order(engine, plans).await
}

#[cfg(test)]
mod tests {
    use super::*;
    use nodedb_physical::physical_plan::{MetaOp, PhysicalPlan};

    /// Verify that an empty plan list returns an empty QueryResult without error.
    #[tokio::test]
    async fn txn_batch_empty_plans_ok() {
        // We can't easily construct a full LiteQueryEngine in a unit test, but
        // we can test the degenerate case: zero plans → immediately returns
        // QueryResult::empty(). The function only calls the loop body when
        // plans is non-empty, so no engine access occurs.
        let plans: Vec<PhysicalPlan> = vec![];
        // Use a Checkpoint plan (no-op in the meta dispatcher) to test non-empty.
        let checkpoint = PhysicalPlan::Meta(MetaOp::Checkpoint);
        assert_eq!(plans.len(), 0);
        // Confirm the Checkpoint variant exists and is Clone.
        let _ = checkpoint.clone();
    }

    #[test]
    fn calvin_passive_is_empty() {
        // handle_calvin_passive is pure async; verify it compiles and is callable.
        let _fut = handle_calvin_passive();
    }
}