name: fjall-guide description: Reference guide for using fjall storage engine in ECSdb
fjall Storage Engine Guide
fjall is the embedded LSM-tree storage engine that powers ECSdb. This guide covers its API and best practices.
fjall Overview
fjall is a pure Rust LSM-tree (Log-Structured Merge-tree) storage engine. It provides:
- Write-Ahead Log (WAL) for durability
- MemTable (skip list) for in-memory buffering
- SSTables (Sorted String Tables) for on-disk storage
- Background compaction
- Snapshots for consistent reads
- Partitions (our "column families")
Core Concepts
Keyspace
The top-level container. One keyspace per database.
use fjall::{Config, Keyspace};
let keyspace = Config::new(&data_dir)
.max_write_buffer_size(64 * 1024 * 1024) // 64MB memtable
.open()?;
Partition
Like a column family - a separate key-value namespace.
let partition = keyspace.open_partition("component_Position", Default::default())?;
Basic Operations
// Write
partition.insert(key, value)?;
// Read
let value = partition.get(key)?; // Returns Option<Arc<[u8]>>
// Delete
partition.remove(key)?;
// Persist (sync to disk)
keyspace.persist()?;
Range Scans
// Prefix scan
for result in partition.prefix(b"entity_") {
let (key, value) = result?;
// process
}
// Range scan
use std::ops::Bound;
for result in partition.range(start_key..end_key) {
let (key, value) = result?;
}
Snapshots
For consistent reads (MVCC):
let snapshot = keyspace.snapshot();
let value = partition.get_with_snapshot(key, &snapshot)?;
ECSdb Usage Patterns
Column Family Naming
const METADATA_CF: &str = "_metadata";
const EDGES_CF: &str = "_edges";
const SCHEMAS_CF: &str = "_schemas";
fn component_cf(name: &str) -> String {
format!("component_{}", name)
}
Key Encoding
Keys must be lexicographically sortable:
// Entity key: [uuid:16]
fn encode_entity_key(id: Uuid) -> [u8; 16] {
*id.as_bytes()
}
// Component key: [entity_id:16][version:8]
fn encode_component_key(entity_id: Uuid, version: u64) -> [u8; 24] {
let mut key = [0u8; 24];
key[0..16].copy_from_slice(entity_id.as_bytes());
key[16..24].copy_from_slice(&version.to_be_bytes());
key
}
Batch Writes
For atomicity:
// fjall doesn't have explicit batches like RocksDB
// Use persist() after a group of writes
partition1.insert(key1, value1)?;
partition2.insert(key2, value2)?;
keyspace.persist()?; // Atomic durability point
Error Handling
use fjall::Result as FjallResult;
fn storage_op() -> Result<(), StorageError> {
partition.insert(key, value)
.map_err(|e| StorageError::Backend(e.to_string()))?;
Ok(())
}
Performance Tuning
MemTable Size
Larger = fewer flushes, more memory:
Config::new(&path)
.max_write_buffer_size(128 * 1024 * 1024) // 128MB for write-heavy
Block Cache
For read-heavy workloads:
Config::new(&path)
.block_cache_capacity(512 * 1024 * 1024) // 512MB cache
Compression
fjall handles compression internally. Configure via partition config if available.
Common Pitfalls
- Forgetting to persist: Writes are in memory until
persist() - Key ordering: Use big-endian for numeric keys
- Partition lifecycle: Open partitions are cached, don't reopen frequently
- Snapshot scope: Snapshots hold resources, drop when done
Testing with fjall
#[cfg(test)]
mod tests {
use tempfile::TempDir;
fn create_test_keyspace() -> Keyspace {
let temp = TempDir::new().unwrap();
Config::new(temp.path()).open().unwrap()
}
#[test]
fn test_basic_ops() {
let ks = create_test_keyspace();
let part = ks.open_partition("test", Default::default()).unwrap();
part.insert(b"key", b"value").unwrap();
assert_eq!(part.get(b"key").unwrap().as_deref(), Some(b"value".as_slice()));
}
}
Documentation Links
- fjall crate: https://crates.io/crates/fjall
- fjall docs: https://docs.rs/fjall