boogie
A suite of WAL-based data stores
Summary
| Latest Version | 0.2.0 |
|---|---|
| License | LGPL-3.0-or-later |
| CI Status | Failing |
| Downloads | 0 |
| Last Indexed | 2026-09-05 07:26 |
Tags
Authors
- George Lemon
Installation
nimble install boogie
choosenim install boogie
git clone https://github.com/openpeeps/boogie
OS Compatibility
| Platform | Linux | macOS | Windows | FreeBSD | OpenBSD | NetBSD | Android | iOS | WASM | Embedded |
|---|---|---|---|---|---|---|---|---|---|---|
| boogie | ✓ | ✓ | ✓ | - | - | - | - | - | - | - |
Dependencies
| Package | Version | Optional |
|---|---|---|
| nim >= | 2.0.0 | No |
| sorta >= | 0.2.0 | No |
| openparser >= | 0.2.0 | No |
| vancode >= | 0.2.6 | No |
| voodoo >= | 0.2.0 | No |
| db_connector >= | 0.1.0 | No |
Source
| Repository | https://github.com/openpeeps/boogie |
|---|---|
| Homepage | https://github.com/openpeeps/boogie |
| Registry Source | nimble_official |
README

A suite of embedded data stores in Nim with
write-ahead log (WAL) support for durability and crash recovery
nimble install boogie
😍 Key Features
- BTrees storage and Hash Tables for fast lookups
- Write-ahead log (WAL) for durability and crash recovery, with batched group commits, CRC32 record framing, and O(1) LSN recovery
- Simple API for inserting, updating, deleting, and querying records
- Configurable options for performance tuning, such as batch sizes and flush intervals
- In-memory or On-disk storage modes
- Primitive data types (
string,int,float,bool,json,null)
What's included?
- Key/Value Store - A simple Key-Value store implementation with WAL support, plus an optional lazy-read mode that keeps values on disk
- RDBMS Store - A relational WAL-based database with support for schemas, primary keys, typed columns and foreign keys with
RESTRICTdelete actions - Vector Store - Vector store implementation with WAL support and nearest-neighbor search (cosine / dot / L2), including named partitions that bound a search to a locality scope
- Columnar Store - Columnar storage engine for analytics workloads with WAL support and in-memory column caching
- Graph Store - A simple graph database with support for nodes, relationships, and basic graph queries (e.g., neighbors, BFS) with WAL support
- Document Store - A flexible document store on top of the WAL, storing JSON (or BSON) documents
- SQL Driver - A
db_connector-compatible driver (boogie/db_boogie) that compiles SQL statements to vancode bytecode and evaluates them against the RDBMS store
[!NOTE] Boogie is an experimental project mostly made with the chatbot for fun and learning. It is still in early stages, so expect data loss and breaking changes. Use at your own risk.
This can be used as a simple embedded database for your Nim applications. If you want, you can use openpeeps/e2ee to encrypt the data before inserting it into Boogie database.
Examples
Every store supports an in-memory mode and a disk-backed mode with WAL, group
commits, and periodic checkpoints. Benchmark numbers come from the bundled test
suites (nim r -d:release tests/testX.nim) on the author's development machine.
Key/Value Store
import boogie/stores/kv
import std/options
let kvStore = newInMemoryKvStore() # or newKvStore("mydb", ksmDisk, enableWal = true)
kvStore.put("greeting", "hello")
kvStore.put("user:1", "alice")
if kvStore.hasKey("greeting"):
echo kvStore.get("greeting").get() # "hello"
discard kvStore.delete("user:1") # true
for k, v in kvStore.pairsUnordered: # unordered iteration
echo k, " -> ", v
For very large datasets,
newKvStore("mydb", ksmDisk, enableWal = true, lazyReads = true)keeps only a key → offset index in memory and reads values back from the WAL on demand. Values are never resident in RAM (0 bytes retained for 50k×256B in the benchmark), at the cost of a disk seek+read pergetinstead of a memory lookup.
| Operation | ops/s |
|---|---|
| put (memory) | ~3.13 M |
| get (memory) | ~4.75 M |
| delete (memory) | ~4.08 M |
| put (disk + WAL) | ~1.39 M |
| get (disk + WAL) | ~7.52 M |
| delete (disk + WAL) | ~2.18 M |
| get (disk, lazy offset read) | ~0.06 M |
RDBMS Store
import boogie/stores/rdbms
import std/options
let store = newStore("blogdata", smDisk, enableWal = true, checkpointEveryOps = 10)
let users = newTable(
name = "users",
primaryKey = "id",
columns = [
newColumn("id", dtInt, false),
newColumn("name", dtText, false),
newColumn("email", dtText, true)
],
primaryKeyMode = pkmSerial
)
store.createTableIfNotExist(users)
# Auto-increment primary key
let userPk = store.insertRow("users", row({
"name": newTextValue("Alice"),
"email": newTextValue("alice@example.com")
}))
# Explicit primary key
store.insertRow("users", "42", row({"name": newTextValue("Bob")}))
# Point lookup + indexed equality search
echo store.getRow("users", userPk).isSome
let usersT = store.getTable("users").get()
usersT.createIndex("name")
for (pk, r) in usersT.where("name", newTextValue("Alice")):
echo pk, " -> ", r
discard store.deleteRow("users", userPk)
store.checkpoint()
| Operation | ops/s |
|---|---|
| insert | ~0.27 M |
| lookup (by pk) | ~0.89 M |
| ordered scan | ~0.12 M |
Vector Store
import boogie/stores/vectorstore
import std/options
let vs = newInMemoryVectorStore() # or newVectorStore("vecdb", smDisk, enableWal = true)
vs.createCollection(newCollection("embeddings", 3))
vs.insert("embeddings", "doc-1", @[0.1'f32, 0.2, 0.3], "tenant-7")
vs.insert("embeddings", "doc-2", @[0.9'f32, 0.8, 0.7], "tenant-7")
echo vs.get("embeddings", "doc-1").isSome
let q = @[0.11'f32, 0.19, 0.31]
# scope the search to a partition to bound the scanned candidate set
for (pk, score) in vs.nearest("embeddings", q, k = 2, dmCosine, "tenant-7"):
echo pk, " (", score, ")"
Vectors can be grouped into named partitions (a locality scope, like a ring in KoutenDB). A partition-scoped
nearestscores only that partition's rows: with 20k vectors across 100 partitions it scans 200 instead of 20,000 (99% reduction) and runs ~50x faster in the benchmark.collection.partitionSize("tenant-7")reports the bounded candidate set size.
| Operation | ops/s |
|---|---|
| insert | ~3.22 M |
| get | ~6.68 M |
| delete | ~4.95 M |
| nearest (k=10, dim=32, 20k vectors) | ~2.17 K queries/s |
| nearest, partition-scoped (1/100 candidate set) | ~116 K queries/s |
Columnar Store
import boogie/stores/columnar
import std/json
var s = openColumnarStore("analytics")
s.createTable(TableSchema(
name: "events",
primaryKey: "id",
rowCount: 0,
columns: @[
ColumnSchema(name: "id", kind: ctInt64, nullable: false, codec: ccNone),
ColumnSchema(name: "user", kind: ctString, nullable: false, codec: ccNone),
ColumnSchema(name: "amount", kind: ctFloat64, nullable: false, codec: ccNone)
]
))
s.insertBatch("events", @[
%*{"id": 1, "user": "alice", "amount": 10.5},
%*{"id": 2, "user": "bob", "amount": 25.0}
])
# Projection + filter
let rows = s.scan("events", @["id", "user"], filters = @[
Filter(column: "amount", op: foGt, value: newJFloat(10.0))
])
# Aggregation
let ag = s.aggregate("events", @[
AggregateSpec(column: "", kind: akCount, alias: "cnt"),
AggregateSpec(column: "amount", kind: akAvg, alias: "avg_amount")
])
echo ag["cnt"].getInt() # 2
Columns are parsed from disk once and cached in memory, so repeated scans/filters over the same columns are much faster than the first (cold) scan.
| Operation | ops/s |
|---|---|
| insert (batch) | ~0.37 M |
| scan (cold, first load) | ~0.43 M |
| scan (warm, cached) | ~3.01 M |
| filter (cached) | ~3.92 M |
Graph Store
import boogie/stores/graphstore
import std/json
var gs = openGraphStore("graphdb")
var tx = beginTx(gs)
let alice = createNode(tx, @["Person"], %*{"name": "Alice"})
let bob = createNode(tx, @["Person"], %*{"name": "Bob"})
discard createRelationship(tx, alice, bob, "KNOWS", %*{"since": 2024})
commit(tx)
echo gs.neighbors(alice) # @[bob]
for r in gs.outgoing(alice, "KNOWS"):
echo r.toId
for n in gs.findNodesByLabel("Person"):
echo n.id
echo gs.traverseBfs(alice, maxDepth = 2).len
closeGraphStore(gs)
| Operation | ops/s |
|---|---|
| commit (nodes + rels) | ~0.11 M |
| getNode | ~6 M |
| neighbors | ~4 M |
| findNodesByLabel (5k nodes) | ~330 µs |
Document Store
import boogie/stores/docstore
import std/json, std/options
var store = openDocumentStore("doctest", name = "documents", defaultEncoding = deJson)
store.insert("k1", %*{"name": "Alice", "age": 30})
store.upsert("k1", %*{"name": "Alice", "age": 31})
if store.hasKey("k1"):
echo store.get("k1").get()["age"] # 31
discard store.delete("k1")
store.checkpoint()
| Operation | ops/s |
|---|---|
| insert | ~0.61 M |
| get | ~1.99 M |
| lookup | ~4.88 M |
| delete | ~0.21 M |
SQL Driver (db_connector compatible)
boogie/db_boogie mirrors the db_connector/db_sqlite API, so applications can
switch to Boogie by changing a single import. Statements are parsed with
openparser/sql, lowered to vancode
bytecode (WHERE predicates run as real VM loops using NULL-aware comparison
opcodes), and evaluated against the RDBMS store, with WAL durability and
crash recovery for free.
import boogie/db_boogie
let db = open("mydb", "", "", "app") # or open(":memory:", "", "", "")
db.exec(sql"CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
age INT DEFAULT 0)")
db.exec(sql"INSERT INTO users (name, age) VALUES (?, ?)", "alice", "30")
echo db.insertID(sql"INSERT INTO users (name) VALUES (?)", "bob")
for row in db.fastRows(sql"SELECT id, name FROM users WHERE age > ? ORDER BY name", "18"):
echo row
# prepared statements with typed bindings
let ps = db.prepare("UPDATE users SET age = age + 1 WHERE id = ?")
ps.bindParam(1, 1'i64)
db.exec(ps)
echo db.getValue(sql"SELECT COUNT(*) FROM users")
db.close()
Supported subset: CREATE TABLE [IF NOT EXISTS] (typed columns, PRIMARY KEY,
NOT NULL, DEFAULT), DROP TABLE [IF EXISTS], CREATE INDEX, multi-row
INSERT with serial primary keys, SELECT with WHERE/ORDER BY/LIMIT/
OFFSET/DISTINCT, UPDATE ... SET ... WHERE, DELETE FROM ... WHERE,
aggregates without GROUP BY (COUNT/SUM/AVG/MIN/MAX), and expressions
with comparisons, AND/OR/NOT, arithmetic, IS [NOT] NULL and ?/$n
placeholders. JOINs, GROUP BY/HAVING and subqueries are not supported yet.
Unlike the SQLite backend, statements here are precompiled bytecode cached per connection; interpolated
exec()calls recompile when their text changes. Numeric comparisons follow SQLite column-affinity rules (text operands coerce when they parse) and NULL propagates per SQL semantics.
| Operation | ops/s |
|---|---|
| insert | ~43 K |
| point query (unindexed scan) | ~281 q/s |
| ordered scan | ~333 K rows/s |
| update | ~89 |
[!TIP] Run the full test + benchmark suites with
nimble test -d:release(the-d:releaseflag is required for accurate benchmarks).
❤ Contributions & Support
- 🐛 Found a bug? Create a new Issue
- 👋 Wanna help? Fork it!
![]() |
Switch to Open-Source LLMs via OpenCode GO, choosing from a variety of powerful models such as DeepSeek, Qwen, Kimi, GLM-5, MiniMax, MiMo. 🍕 Use our referral link to get started! |
🎩 License
LGPLv3 license. Made by Humans from OpenPeeps.
Copyright OpenPeeps & Contributors — All rights reserved.
