Grove
KV Drivers

BoltDB Driver

BoltDB (bbolt) embedded driver for Grove KV with emulated TTL, Scan, Batch, CAS, and Transaction support.

The BoltDB driver connects Grove KV to bbolt, a fast pure-Go B+ tree key-value store. bbolt provides ACID transactions, bucket-based namespacing, and crash safety via memory-mapped I/O. No network setup is required -- the database is a single file on disk.

Installation

go get github.com/xraph/grove/kv
go get github.com/xraph/grove/kv/drivers/boltdriver

Connection

import (
    "github.com/xraph/grove/kv"
    "github.com/xraph/grove/kv/drivers/boltdriver"
)

store, err := kv.Open(boltdriver.New(), "/path/to/data.db")
if err != nil {
    log.Fatal(err)
}
defer store.Close()

The DSN is the file path to the database file. The file is created automatically if it does not exist.

Capabilities

CapabilitySupported
TTLYes (emulated via separate bucket)
CAS (SetNX/SetXX)Yes
ScanYes (prefix + glob matching)
BatchYes (MGet/MSet)
PubSubNo
TransactionsYes (native bbolt transactions)
StreamsNo

Embedded Database

BoltDB is an embedded database that runs in-process. There is no separate server to install or manage:

  • Single file -- the entire database is stored in one file
  • No network -- zero latency from network hops
  • ACID transactions -- all writes are transactional with crash recovery
  • Concurrent reads -- multiple goroutines can read simultaneously
  • Single writer -- only one write transaction can run at a time (serialized via file lock)

This makes BoltDB an excellent choice for CLI tools, embedded applications, local caches, and single-node services where simplicity and reliability matter more than horizontal scalability.

TTL (Emulated)

BoltDB does not natively support key expiration. The driver emulates TTL by storing expiry timestamps in a separate kv_ttl bucket. On every read, the driver checks the TTL bucket and returns kv.ErrNotFound for expired keys:

// Set with a TTL
err := store.Set(ctx, "session:abc", data, 30*time.Minute)

// Check remaining TTL
remaining, err := store.TTL(ctx, "session:abc")

// Update TTL on an existing key
err = store.Expire(ctx, "session:abc", 1*time.Hour)

Expired keys are not automatically deleted from disk. They are filtered out on reads and scans. To reclaim space, delete expired keys periodically or use a background cleanup goroutine.

Options

Configure the driver using boltdriver.Option values:

store, err := kv.Open(boltdriver.New(), "/path/to/data.db",
    boltdriver.WithTimeout(5 * time.Second),
    boltdriver.WithFileMode(0600),
    boltdriver.WithBucket("my-data"),
    boltdriver.WithReadOnly(false),
    boltdriver.WithNoGrowSync(false),
)
OptionDefaultDescription
boltdriver.WithTimeout(d)1sTimeout for obtaining a file lock on the database
boltdriver.WithFileMode(mode)0600File permissions for the database file
boltdriver.WithBucket(name)"kv_data"Custom bucket name for storing key-value data
boltdriver.WithReadOnly(bool)falseOpen the database in read-only mode
boltdriver.WithNoGrowSync(bool)falseDisable grow-sync for faster writes (reduced safety)

Native Transactions

For operations that require direct control over bbolt transactions, use the ViewTxn, UpdateTxn, and Batch methods:

Read-Only Transaction

bdb := boltdriver.Unwrap(store)

err := bdb.ViewTxn(func(tx *bbolt.Tx) error {
    bkt := tx.Bucket([]byte("kv_data"))
    val := bkt.Get([]byte("my-key"))
    fmt.Println(string(val))
    return nil
})

Read-Write Transaction

bdb := boltdriver.Unwrap(store)

err := bdb.UpdateTxn(func(tx *bbolt.Tx) error {
    bkt := tx.Bucket([]byte("kv_data"))
    if err := bkt.Put([]byte("key1"), []byte("val1")); err != nil {
        return err
    }
    return bkt.Put([]byte("key2"), []byte("val2"))
})

Batch Writes

For high-throughput writes, use Batch which combines multiple concurrent write transactions into fewer disk syncs:

bdb := boltdriver.Unwrap(store)

// Can be called concurrently from multiple goroutines
err := bdb.Batch(func(tx *bbolt.Tx) error {
    bkt := tx.Bucket([]byte("kv_data"))
    return bkt.Put([]byte("key"), []byte("value"))
})

Scan

BoltDB supports prefix-based key scanning with glob pattern matching:

// Scan all keys with prefix "user:"
err := store.Scan(ctx, "user:*", func(key string) error {
    fmt.Println(key)
    return nil
})

// Scan all keys
err = store.Scan(ctx, "*", func(key string) error {
    fmt.Println(key)
    return nil
})

Unwrap for Direct Access

Access the underlying BoltDB driver or bbolt.DB for operations not exposed by the Grove KV interface:

// Get the BoltDB driver
bdb := boltdriver.Unwrap(store)

// Get the raw bbolt.DB handle
db := boltdriver.UnwrapDB(store)

// Access bbolt directly
err := db.View(func(tx *bbolt.Tx) error {
    // Iterate over all buckets, access stats, etc.
    return tx.ForEach(func(name []byte, b *bbolt.Bucket) error {
        fmt.Printf("bucket: %s (keys: %d)\n", name, b.Stats().KeyN)
        return nil
    })
})

On this page