| Crates.io | qbice |
| lib.rs | qbice |
| version | 0.4.2 |
| created_at | 2025-12-09 11:58:37.857179+00 |
| updated_at | 2026-01-25 10:18:39.113576+00 |
| description | The Query-Based Incremental Computation Engine |
| homepage | |
| repository | https://github.com/Simmypeet/qbice |
| max_upload_size | |
| id | 1975353 |
| size | 278,225 |
QBICE is a high-performance, asynchronous incremental computation framework for Rust. Define your computation as a graph of queries, and QBICE automatically determines what needs to be recomputed when inputs change—minimizing redundant work through intelligent caching, dependency tracking, and advanced optimization techniques.
default - Includes RocksDB backendrocksdb - Enable RocksDB storage backendfjall - Enable fjall storage backendHere's a simple example demonstrating safe division with incremental recomputation:
use std::sync::{
Arc,
atomic::{AtomicUsize, Ordering},
};
use qbice::{
Config, Decode, DefaultConfig, Encode, Engine, Executor,
Identifiable, Query, StableHash, TrackedEngine,
serialize::Plugin,
stable_hash::{SeededStableHasherBuilder, Sip128Hasher},
storage::kv_database::rocksdb::RocksDB,
};
// Define query types
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, StableHash, Identifiable, Encode, Decode)]
pub enum Variable {
A,
B,
}
impl Query for Variable {
type Value = i32;
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, StableHash, Identifiable, Encode, Decode)]
pub struct SafeDivide {
pub numerator: Variable,
pub denominator: Variable,
}
impl Query for SafeDivide {
type Value = Option<i32>;
}
// Define executor
struct SafeDivideExecutor;
impl<C: Config> Executor<SafeDivide, C> for SafeDivideExecutor {
async fn execute(
&self,
query: &SafeDivide,
engine: &TrackedEngine<C>,
) -> Option<i32> {
let num = engine.query(&query.numerator).await;
let denom = engine.query(&query.denominator).await;
if denom == 0 {
return None;
}
Some(num / denom)
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let temp_dir = tempfile::tempdir()?;
// Create and configure the engine
let mut engine = Engine::<DefaultConfig>::new_with(
Plugin::default(),
RocksDB::factory(temp_dir.path()),
SeededStableHasherBuilder::<Sip128Hasher>::new(0),
)?;
// Register executor
engine.register_executor(Arc::new(SafeDivideExecutor));
// Set initial inputs
{
let mut input_session = engine.input_session();
input_session.set_input(Variable::A, 42);
input_session.set_input(Variable::B, 2);
}
// Execute query
let tracked_engine = Arc::new(engine).tracked();
let result = tracked_engine.query(&SafeDivide {
numerator: Variable::A,
denominator: Variable::B,
}).await;
assert_eq!(result, Some(21));
Ok(())
}
A query represents a unit of computation with an associated input (the query key) and output (the query value). Queries implement the Query trait and are identified by their type and a stable hash of their contents.
Required traits for queries:
StableHash - For consistent hashing across program runsIdentifiable - For stable type identificationEq + Hash - For use in hash mapsClone - For storing query keysDebug - For error messages and debuggingSend + Sync - For thread-safe accessAn executor defines how to compute the value for a specific query type. Executors implement the Executor trait and can depend on other queries through the TrackedEngine.
The Engine is the central database that stores computed values and manages the dependency graph. It tracks which queries depend on which other queries and handles cache invalidation when inputs change.
The TrackedEngine is a wrapper around Engine that tracks dependencies during query execution. Use it to execute queries and build the dependency graph automatically.
In large dependency graphs, a single input change can cause excessive dirty propagation through "chokepoint" queries that have many dependents. Firewall queries prevent dirty propagation from crossing them, limiting the scope of dirty propagation.
A firewall query only propagates dirtiness to its dependents if its computed value actually changes. This is particularly useful for global analysis queries that produce large results. The firewall query also works best with projection queries to minimize unnecessary invalidations.
impl Query for GlobalAnalysis {
type Value = HashMap<String, Type>;
fn execution_style() -> ExecutionStyle {
ExecutionStyle::Firewall
}
}
Projection queries work with firewall queries to provide fine-grained change detection. They read a small part of a firewall query's output and are very fast to compute (e.g., hash map lookup). When a firewall query changes, projection queries are re-executed to check if their specific slice of data changed, preventing unnecessary invalidation of downstream queries.
impl Query for GetFunctionType {
type Value = Option<Type>;
fn execution_style() -> ExecutionStyle {
ExecutionStyle::Projection
}
}
QBICE uses a monotonic timestamp system to ensure each query is verified at most once per mutation. When inputs change, the engine's timestamp increments. During repairation, queries compare their last verification timestamp with the current timestamp to avoid redundant checks.
┌─────────────────────────────────────────────────────────────────┐
│ Engine<C> │
│ ┌─────────────────────┐ ┌─────────────────────────────┐ │
│ │ Query Database │ │ Executor Registry │ │
│ │ - Cached values │ │ - Query type → Executor │ │
│ │ - Dependencies │ └─────────────────────────────┘ │
│ │ - Dirty flags │ │
│ │ - Fingerprints │ │
│ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
│ Arc::new(engine).tracked()
▼
┌─────────────────────────────────────────────────────────────────┐
│ TrackedEngine<C> │
│ - Reference to Engine │
│ - Local query cache │
│ - Caller tracking for dependencies │
└─────────────────────────────────────────────────────────────────┘
QBICE supports persistent storage of query results across program restarts. Computed values and metadata are stored in a pluggable key-value database:
// Results persist across program runs
let engine1 = Engine::<DefaultConfig>::new_with(
Plugin::default(),
RocksDB::factory(&db_path),
hasher,
)?;
// ... compute and store results ...
// Later, in a new process
let engine2 = Engine::<DefaultConfig>::new_with(
Plugin::default(),
RocksDB::factory(&db_path), // Same path
hasher,
)?;
// Previous results are loaded automatically
For detailed documentation, examples, and API reference:
QBICE is designed for scenarios where:
Best practices:
Arc, small Copy types)QBICE is inspired by:
This project is licensed under the MIT License - see the LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.
Created by Simmypeet