bbolt

package module
v0.0.0-...-7390a75 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 4 Imported by: 0

README

go-ruby-bbolt/bbolt

bbolt — go-ruby-bbolt

Docs License Go Coverage

A clean, idiomatic Ruby-style key/value store API over go.etcd.io/bbolt — the pure-Go, embedded, ACID B+tree store.

bbolt is Go-native: there is no canonical Ruby bbolt gem to reimplement. This module instead designs the surface a Ruby Bolt gem would have — buckets, ACID transactions, and cursors — the way a Rubyist expects to use an embedded store, and implements it directly on top of the real bbolt engine. It exposes a Go capability as an idiomatic Ruby API: it sits alongside Ruby's own PStore and GDBM idioms, but, unlike them, offers genuine MVCC ACID transactions — one writer and many concurrent readers over a single memory-mapped file, with commit/rollback.

The storage engine — the copy-on-write B+tree, the page cache, the freelist, the mmap, and the write-ahead crash safety — is bbolt's and is not reimplemented here. This module is a thin, allocation-conscious façade that renames bbolt's Go surface to the shape a Ruby binding wants and maps bbolt's sentinel errors onto a Bolt::Error class tree the host can raise. The engine is pure Go, so the whole stack links statically with CGO_ENABLED=0 on every 64-bit target the go-* ecosystem supports, and on Linux, macOS and Windows.

It is a sibling of go-ruby-sqlite3 (the pure-Go SQLite backend) and go-ruby-pstore (the PStore port), and is a standalone, reusable module for go-embedded-ruby.

Ruby surface

Ruby (Bolt) Go
Bolt::DB.open(path, options) Open(path, opts)
Bolt.open(path) Open(path, nil)
db.close / db.path / db.stats DB.Close / Path / Stats
db.update { |tx| … } DB.Update (read-write)
db.view { |tx| … } DB.View (read-only)
db.begin(writable) DB.BeginTx.Commit/Rollback
tx.bucket(name) / create_bucket Tx.Bucket / CreateBucket
tx.buckets Tx.Buckets
bucket.put/get/delete Bucket.Put / Get / Delete
bucket.each { |k, v| … } Bucket.Each
bucket.cursor Bucket.Cursor
bucket.bucket(name) (nested) Bucket.Bucket / CreateBucket
bucket.next_sequence Bucket.NextSequence
cursor.first/next/prev/last/seek/delete Cursor.First / Next / …

Keys and values are []byte throughout — a Ruby host maps them to and from ASCII-8BIT (binary) Strings. Using []byte (rather than string) keeps binary safety and preserves bbolt's distinction between a missing key (Get returns a nil slice) and a present, empty value (a non-nil, zero-length slice).

Install

go get github.com/go-ruby-bbolt/bbolt

Usage

package main

import (
	"fmt"

	bbolt "github.com/go-ruby-bbolt/bbolt"
)

func main() {
	db, _ := bbolt.Open("app.db", nil) // Bolt.open("app.db")
	defer db.Close()

	// Read-write transaction (Bolt::DB#update): commits on success,
	// rolls back on any returned error or panic.
	db.Update(func(tx *bbolt.Tx) error {
		users, err := tx.CreateBucketIfNotExists([]byte("users"))
		if err != nil {
			return err
		}
		id, _ := users.NextSequence()
		return users.Put([]byte(fmt.Sprintf("%d", id)), []byte("alice"))
	})

	// Read-only transaction (Bolt::DB#view) with a cursor.
	db.View(func(tx *bbolt.Tx) error {
		c := tx.Bucket([]byte("users")).Cursor()
		for k, v := c.First(); k != nil; k, v = c.Next() {
			fmt.Printf("%s = %s\n", k, v)
		}
		return nil
	})
}

Error tree

Errors are *bbolt.Error, each naming the Bolt:: exception class a host should raise and wrapping the underlying bbolt sentinel, so both this package's sentinel and bbolt's own report through errors.Is:

err := db.Update(func(tx *bbolt.Tx) error {
	_, err := tx.CreateBucket([]byte("dup"))
	if err != nil {
		return err
	}
	_, err = tx.CreateBucket([]byte("dup")) // already exists
	return err
})

var e *bbolt.Error
if errors.As(err, &e) {
	fmt.Println(e.Class) // Bolt::BucketExists
}
errors.Is(err, bbolt.ErrBucketExists)       // true (this package)
errors.Is(err, boltpkg.ErrBucketExists)     // true (go.etcd.io/bbolt)

The tree covers Bolt::DatabaseNotOpen, BucketExists, BucketNotFound, BucketNameRequired, TxClosed, TxNotWritable, DatabaseReadOnly, KeyRequired, KeyTooLarge, ValueTooLarge, IncompatibleValue, Timeout, Invalid, VersionMismatch, Checksum, and more — one per bbolt sentinel. A caller's own error returned from an Update block to force a rollback is passed back unchanged.

Backend & architectures

The engine is go.etcd.io/bbolt — a real, embedded, ACID B+tree, no cgo. Every arch below builds and tests with CGO_ENABLED=0:

arch CGO=0 build notes
amd64 native CI lane
arm64 native CI lane
riscv64 qemu-user CI lane
loong64 qemu-user CI lane
ppc64le qemu-user CI lane
s390x qemu-user CI lane (big-endian)

The -race host lane keeps the default toolchain (cgo on for the race detector); the backend is pure-Go either way. Windows is a first-class host lane — bbolt's mmap layer supports it.

Cross-endian file-portability caveat

bbolt stores a native-endian magic number in its file header and validates it on open, so a database file is not portable across byte orders: a file written on a little-endian host (amd64/arm64/riscv64/loong64/ppc64le) will fail to open on a big-endian host (s390x), and vice versa. This is a property of the bbolt on-disk format, not of this binding, and it is not a problem here: every test writes and reads a fresh temp-file database on the same host, so the format round-trips cleanly and the suite is green on s390x (big-endian) as well as the little-endian arches. If you need a portable dump, serialise the logical key/value contents rather than copying the file.

Tests & coverage

Deterministic, interpreter-free tests over temp-file databases (t.TempDir) exercise put/get/delete/iterate in read-write and read-only transactions, nested buckets, cursor seek/first/last/next/prev, sequences, rollback-discards / commit-persists (verified by reopening the DB), concurrent read transactions, and the full error surface (bucket exists/not-found, tx closed, read-only writes). They alone hold coverage at 100%, so the qemu cross-arch and Windows lanes pass the gate with no network and no external oracle.

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-bbolt/bbolt authors.

WebAssembly

Unlike the rest of the go-ruby family, this library does not target WebAssembly: its backing engine — go.etcd.io/bbolt (memory-mapped files + file locking) — relies on mmap and native filesystem syscalls that the js/wasm and wasip1/wasm sandboxes do not provide. It ships for the six 64-bit native/qemu arches only.

Documentation

Overview

Package bbolt exposes go.etcd.io/bbolt — the pure-Go, embedded, ACID key/value store — as a clean, idiomatic Ruby-style API for the Ruby / rbgo ecosystem.

What it is — and isn't

bbolt is Go-native: there is no canonical Ruby "bbolt" gem to reimplement. This module instead designs the surface a Ruby "Bolt" gem *would* have — buckets, ACID transactions, and cursors — the way a Rubyist expects to use an embedded store, and implements it directly on top of the real bbolt B+tree engine. It sits alongside Ruby's own persistence idioms (the standard library's PStore and GDBM) but, unlike them, offers genuine multi-version concurrency-control ACID transactions: one writer and many concurrent readers over a single memory-mapped file, with commit/rollback semantics.

The storage engine — the copy-on-write B+tree, the page cache, the freelist, the mmap, and the write-ahead crash safety — is bbolt's and is *not* reimplemented here. This package is a thin, faithful, allocation-conscious façade that renames bbolt's Go surface to the shape a Ruby binding wants and maps bbolt's sentinel errors onto a Bolt::Error class tree the host can raise.

The engine is pure Go, so the whole stack links statically with CGO_ENABLED=0 on every 64-bit target the go-* ecosystem supports (amd64, arm64, riscv64, loong64, ppc64le, s390x), and on Linux, macOS and Windows (bbolt's mmap layer supports all three).

Ruby surface

Bolt::DB.open(path, options)   -> Open(path, opts)
Bolt.open(path)                -> Open(path, nil)
db.close / db.path / db.stats  -> DB.Close / DB.Path / DB.Stats
db.update { |tx| ... }         -> DB.Update  (read-write, auto commit/rollback)
db.view   { |tx| ... }         -> DB.View    (read-only)
db.begin(writable)             -> DB.Begin -> Tx.Commit / Tx.Rollback
tx.bucket / create_bucket ...  -> Tx.Bucket / CreateBucket / ...
bucket.put / get / delete      -> Bucket.Put / Get / Delete
bucket.each { |k, v| ... }     -> Bucket.Each
bucket.cursor                  -> Cursor.First/Next/Prev/Last/Seek/Delete
bucket.next_sequence           -> Bucket.NextSequence

Keys and values are []byte throughout. A Ruby host maps them to and from ASCII-8BIT (binary) Strings; using []byte (rather than string) keeps binary safety and preserves bbolt's distinction between a missing key (Get returns a nil slice) and a present, empty value (a non-nil, zero-length slice).

Cross-endian file-portability caveat

bbolt stores a native-endian magic number in its file header and validates it on open. A database file is therefore *not* portable across byte orders: a file written on a little-endian host (amd64, arm64, riscv64, loong64, ppc64le) will fail to open on a big-endian host (s390x), and vice versa, with a "version mismatch" / "invalid database" error. This is a property of the bbolt on-disk format, not of this binding. It is not a problem in practice here: every test writes and reads a fresh temp-file database on the *same* host, so the format round-trips cleanly, and the suite is green on s390x (big-endian) as well as the little-endian arches. If you need a portable dump, serialise the logical key/value contents rather than copying the file.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrDatabaseNotOpen    = &Error{Class: "Bolt::DatabaseNotOpen", err: bolt.ErrDatabaseNotOpen}
	ErrInvalid            = &Error{Class: "Bolt::Invalid", err: bolt.ErrInvalid}
	ErrInvalidMapping     = &Error{Class: "Bolt::InvalidMapping", err: bolt.ErrInvalidMapping}
	ErrVersionMismatch    = &Error{Class: "Bolt::VersionMismatch", err: bolt.ErrVersionMismatch}
	ErrChecksum           = &Error{Class: "Bolt::Checksum", err: bolt.ErrChecksum}
	ErrTimeout            = &Error{Class: "Bolt::Timeout", err: bolt.ErrTimeout}
	ErrTxNotWritable      = &Error{Class: "Bolt::TxNotWritable", err: bolt.ErrTxNotWritable}
	ErrTxClosed           = &Error{Class: "Bolt::TxClosed", err: bolt.ErrTxClosed}
	ErrDatabaseReadOnly   = &Error{Class: "Bolt::DatabaseReadOnly", err: bolt.ErrDatabaseReadOnly}
	ErrFreePagesNotLoaded = &Error{Class: "Bolt::FreePagesNotLoaded", err: bolt.ErrFreePagesNotLoaded}
	ErrBucketNotFound     = &Error{Class: "Bolt::BucketNotFound", err: bolt.ErrBucketNotFound}
	ErrBucketExists       = &Error{Class: "Bolt::BucketExists", err: bolt.ErrBucketExists}
	ErrBucketNameRequired = &Error{Class: "Bolt::BucketNameRequired", err: bolt.ErrBucketNameRequired}
	ErrKeyRequired        = &Error{Class: "Bolt::KeyRequired", err: bolt.ErrKeyRequired}
	ErrKeyTooLarge        = &Error{Class: "Bolt::KeyTooLarge", err: bolt.ErrKeyTooLarge}
	ErrValueTooLarge      = &Error{Class: "Bolt::ValueTooLarge", err: bolt.ErrValueTooLarge}
	ErrIncompatibleValue  = &Error{Class: "Bolt::IncompatibleValue", err: bolt.ErrIncompatibleValue}
)

The Bolt::Error tree. Each value maps one bbolt sentinel onto the Ruby exception class a host should raise.

Functions

This section is empty.

Types

type Bucket

type Bucket struct {
	// contains filtered or unexported fields
}

Bucket is a Bolt bucket — the Ruby Bolt::Bucket — a named, ordered collection of key/value pairs that may also hold nested sub-buckets.

func (*Bucket) Bucket

func (b *Bucket) Bucket(name []byte) *Bucket

Bucket returns the nested sub-bucket with the given name, or nil if it does not exist (Bolt::Bucket#bucket).

func (*Bucket) Buckets

func (b *Bucket) Buckets() [][]byte

Buckets returns the names of the immediate nested sub-buckets, in byte order (Bolt::Bucket#buckets). Each returned slice is a copy owned by the caller.

func (*Bucket) CreateBucket

func (b *Bucket) CreateBucket(name []byte) (*Bucket, error)

CreateBucket creates a nested sub-bucket (Bolt::Bucket#create_bucket). It fails with ErrBucketExists if the sub-bucket already exists.

func (*Bucket) CreateBucketIfNotExists

func (b *Bucket) CreateBucketIfNotExists(name []byte) (*Bucket, error)

CreateBucketIfNotExists creates a nested sub-bucket if it does not already exist (Bolt::Bucket#create_bucket_if_not_exists).

func (*Bucket) Cursor

func (b *Bucket) Cursor() *Cursor

Cursor returns a cursor over the bucket (Bolt::Bucket#cursor).

func (*Bucket) Delete

func (b *Bucket) Delete(key []byte) error

Delete removes key from the bucket, if present (Bolt::Bucket#delete).

func (*Bucket) DeleteBucket

func (b *Bucket) DeleteBucket(name []byte) error

DeleteBucket removes a nested sub-bucket (Bolt::Bucket#delete_bucket).

func (*Bucket) Each

func (b *Bucket) Each(fn func(key, value []byte) error) error

Each iterates every key/value pair in byte order (Bolt::Bucket#each). If fn returns an error, iteration stops and that error is returned. Keys that map to nested buckets are yielded with a nil value.

func (*Bucket) Get

func (b *Bucket) Get(key []byte) []byte

Get returns the value for key, or a nil slice if the key is absent (Bolt::Bucket#get). A present but empty value is a non-nil, zero-length slice. The returned slice is only valid for the life of the transaction and must not be mutated.

func (*Bucket) NextSequence

func (b *Bucket) NextSequence() (uint64, error)

NextSequence returns a new, auto-incrementing sequence value unique within the bucket (Bolt::Bucket#next_sequence).

func (*Bucket) Put

func (b *Bucket) Put(key, value []byte) error

Put stores value under key (Bolt::Bucket#put). An empty key fails with ErrKeyRequired; a read-only transaction fails with ErrTxNotWritable.

func (*Bucket) Sequence

func (b *Bucket) Sequence() uint64

Sequence returns the current monotonic sequence counter (Bolt::Bucket#sequence).

func (*Bucket) SetSequence

func (b *Bucket) SetSequence(v uint64) error

SetSequence sets the sequence counter (Bolt::Bucket#sequence=).

func (*Bucket) Writable

func (b *Bucket) Writable() bool

Writable reports whether the bucket belongs to a writable transaction.

type Cursor

type Cursor struct {
	// contains filtered or unexported fields
}

Cursor iterates the keys of a bucket in byte order — the Ruby Bolt::Cursor. A nil key returned by any of the movement methods means the cursor has moved past the end (or before the start) of the bucket. When a key names a nested sub-bucket, its value is nil.

func (*Cursor) Delete

func (c *Cursor) Delete() error

Delete removes the key/value pair the cursor currently points at (Bolt::Cursor#delete).

func (*Cursor) First

func (c *Cursor) First() (key, value []byte)

First moves to the first key/value pair (Bolt::Cursor#first).

func (*Cursor) Last

func (c *Cursor) Last() (key, value []byte)

Last moves to the last key/value pair (Bolt::Cursor#last).

func (*Cursor) Next

func (c *Cursor) Next() (key, value []byte)

Next moves to the next key/value pair (Bolt::Cursor#next).

func (*Cursor) Prev

func (c *Cursor) Prev() (key, value []byte)

Prev moves to the previous key/value pair (Bolt::Cursor#prev).

func (*Cursor) Seek

func (c *Cursor) Seek(seek []byte) (key, value []byte)

Seek moves to the first key at or after seek (Bolt::Cursor#seek). If no such key exists it returns a nil key.

type DB

type DB struct {
	// contains filtered or unexported fields
}

DB is an open Bolt database — the Ruby Bolt::DB. It wraps a single memory-mapped bbolt file and hands out read-write and read-only transactions.

func Open

func Open(path string, opts *Options) (*DB, error)

Open opens (creating if necessary) the Bolt database at path. It is both Bolt::DB.open(path, options) and, with a nil opts, Bolt.open(path).

func (*DB) Begin

func (d *DB) Begin(writable bool) (*Tx, error)

Begin starts an explicit transaction (Bolt::DB#begin). Pass writable=true for a read-write transaction. The caller must eventually call Tx.Commit or Tx.Rollback. Only one writable transaction may be open at a time; any number of read-only transactions may run concurrently.

func (*DB) Close

func (d *DB) Close() error

Close releases all resources and unmaps the file (Bolt::DB#close).

func (*DB) IsReadOnly

func (d *DB) IsReadOnly() bool

IsReadOnly reports whether the database was opened read-only.

func (*DB) Path

func (d *DB) Path() string

Path returns the path to the database file (Bolt::DB#path).

func (*DB) Stats

func (d *DB) Stats() Stats

Stats returns a snapshot of database-level statistics (Bolt::DB#stats).

func (*DB) Update

func (d *DB) Update(fn func(*Tx) error) error

Update runs fn inside a read-write transaction (Bolt::DB#update). If fn returns nil the transaction is committed; if it returns an error or panics, the transaction is rolled back and the error is returned unchanged.

func (*DB) View

func (d *DB) View(fn func(*Tx) error) error

View runs fn inside a read-only transaction (Bolt::DB#view). Writes attempted inside fn fail with ErrTxNotWritable.

type Error

type Error struct {
	// Class is the Ruby exception class the host should raise, e.g.
	// "Bolt::BucketNotFound".
	Class string
	// contains filtered or unexported fields
}

Error is a Bolt error. It names the Ruby exception class a host binding should raise (Class) and wraps the underlying bbolt sentinel, so both

errors.Is(err, bbolt.ErrBucketNotFound)      // this package's sentinel
errors.Is(err, bolt.ErrBucketNotFound)       // go.etcd.io/bbolt's sentinel

report true.

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface, reporting the underlying bbolt message.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap exposes the underlying go.etcd.io/bbolt sentinel so errors.Is/As reach through to it.

type Options

type Options struct {
	// Mode is the file mode used when the database file is created. Zero means
	// 0600.
	Mode os.FileMode

	// ReadOnly opens the database in read-only mode, taking a shared lock. Write
	// transactions then fail with ErrDatabaseReadOnly.
	ReadOnly bool

	// Timeout is how long Open waits to obtain the file lock. Zero waits
	// indefinitely; on a locked file a non-zero timeout fails with ErrTimeout.
	Timeout time.Duration

	// NoGrowSync sets bbolt's NoGrowSync flag before memory-mapping the file.
	NoGrowSync bool

	// NoFreelistSync avoids syncing the freelist to disk, trading recovery cost
	// for write throughput.
	NoFreelistSync bool
}

Options configures Open, mirroring the option hash a Ruby Bolt::DB.open would accept. The zero value is valid and yields a read-write database created with mode 0600.

type Stats

type Stats struct {
	// TxN is the total number of started read transactions.
	TxN int
	// OpenTxN is the number of currently open read transactions.
	OpenTxN int
	// FreePageN is the number of free pages on the freelist.
	FreePageN int
	// PendingPageN is the number of pending pages on the freelist.
	PendingPageN int
	// FreeAlloc is the bytes allocated in free pages.
	FreeAlloc int
	// FreelistInuse is the bytes used by the freelist.
	FreelistInuse int
}

Stats is a snapshot of database-level counters (Bolt::DB#stats).

type Tx

type Tx struct {
	// contains filtered or unexported fields
}

Tx is a Bolt transaction — the Ruby Bolt::Tx. A writable transaction may create, delete and mutate buckets; a read-only one may only read. A Tx is not safe for concurrent use by multiple goroutines.

func (*Tx) Bucket

func (tx *Tx) Bucket(name []byte) *Bucket

Bucket returns the top-level bucket with the given name, or nil if it does not exist (Bolt::Tx#bucket).

func (*Tx) Buckets

func (tx *Tx) Buckets() [][]byte

Buckets returns the names of all top-level buckets, in byte order (Bolt::Tx#buckets). Each returned slice is a copy owned by the caller.

func (*Tx) Commit

func (tx *Tx) Commit() error

Commit writes all changes and closes the transaction (Bolt::Tx#commit). Committing a read-only transaction, or a transaction already finished, fails.

func (*Tx) CreateBucket

func (tx *Tx) CreateBucket(name []byte) (*Bucket, error)

CreateBucket creates a new top-level bucket (Bolt::Tx#create_bucket). It fails with ErrBucketExists if the bucket already exists.

func (*Tx) CreateBucketIfNotExists

func (tx *Tx) CreateBucketIfNotExists(name []byte) (*Bucket, error)

CreateBucketIfNotExists creates a top-level bucket if it does not already exist (Bolt::Tx#create_bucket_if_not_exists).

func (*Tx) DeleteBucket

func (tx *Tx) DeleteBucket(name []byte) error

DeleteBucket removes a top-level bucket (Bolt::Tx#delete_bucket). It fails with ErrBucketNotFound if the bucket does not exist.

func (*Tx) ID

func (tx *Tx) ID() int

ID returns the transaction id (Bolt::Tx#id).

func (*Tx) Rollback

func (tx *Tx) Rollback() error

Rollback discards all changes and closes the transaction (Bolt::Tx#rollback).

func (*Tx) Writable

func (tx *Tx) Writable() bool

Writable reports whether the transaction can perform writes.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL