BadgerHold Documentation

repository·master·Indexed 20 days ago

https://github.com/timshannon/badgerhold

A querying and indexing layer for the Badger KV database that allows developers to interact with structured Go types instead of raw byte slices. It provides features such as type prefixing, custom serialization via Gob, struct-tag based indexing, unique constraints, and auto-incrementing keys. The library includes a chainable query API for filtering, bulk operations (DeleteMatching, UpdateMatching), aggregate queries (Min, Max, Avg, Sum), and a ForEach method for iterating over large datasets.

Tokens
11.8K
Snippets
56
Records
65
Agent score
67%

What's inside BadgerHold

  1. What is BadgerHold?

    master

    BadgerHold is a high-level querying and indexing layer built on top of the Badger key-value database. It simplifies working with Go types by providing a way to store and find data using structured types rather than raw byte slices.

    Key features include:

    • Type Prefixing: Each Go type is automatically prefixed with its type name, allowing you to store multiple different types in a single Badger database without key collisions.
    • Custom Serialization: By default, BadgerHold uses Gob encoding. You can optimize performance by implementing the GobEncoder/Decoder interfaces or provide custom encode/decode functions via the Options struct during Open.
  2. How indexes work in BadgerHold

    master

    Indexes in BadgerHold allow you to skip scanning records that do not meet specific criteria, significantly improving query performance for read-heavy datasets.

    How they work:

    • Indexes are stored in a reserved bucket named _indexes.
    • They map index values back to the primary keys of the records they belong to.
    • Trade-off: While indexes speed up reads, they increase disk I/O because every write operation must also update the relevant indexes.

    You can define indexes using struct tags or by implementing the Storer interface for manual control.

  3. Configure struct keys and auto-incrementing IDs

    master

    BadgerHold allows you to map database keys directly into your Go structs and supports auto-incrementing keys.

    Mapping Keys: Use the badgerhold:"key" struct tag to automatically populate a record's key into a field during Find queries.

    Auto-incrementing Keys: If a field tagged with badgerhold:"key" is of type uint64 and contains its zero-value during an Insert, BadgerHold will automatically set the key before insertion. To explicitly use an auto-incrementing key, pass badgerhold.NextSequence() as the key argument in Insert.

    Note: To retrieve the generated ID from NextSequence(), you must pass a pointer to your data to the Insert method.

    type Employee struct {
    	ID uint64 `badgerhold:"key"` // Automatically populated or set via NextSequence
    	FirstName string
    	LastName string
    }
    
    // Using auto-incrementing sequence
    err := store.Insert(badgerhold.NextSequence(), &data)
  4. When to use BadgerHold vs BadgerDB or SQLite

    master

    BadgerHold is a high-level wrapper for BadgerDB that allows you to work directly with Go types, eliminating the need for manual data filtering code or an ORM layer.

    Use BadgerHold when:

    • You want to use BadgerDB but want to avoid manual serialization/deserialization.
    • You want a simpler alternative to SQLite for Go-centric applications. Unlike SQLite, which requires schema migrations and table creation scripts, BadgerHold allows you to simply open a file and start inserting Go structs immediately without explicit database initialization or schema definitions.
  5. Implement custom comparison logic

    master

    BadgerHold compares types using standard Go rules (types must match exactly, e.g., int vs int32 will fail). For standard library types like time.Time, big.Int, etc., comparison works out of the box.

    To support custom comparison for your own types, implement the Comparer interface by adding a Compare method:

    Compare(other interface{}) (int, error)

    If a type does not implement Comparer, BadgerHold falls back to comparing the string representation of the value lexicographically.

  6. Initialize a BadgerHold store

    master

    To use BadgerHold, you must configure badgerhold.Options and call badgerhold.Open. You should always defer store.Close() to ensure the underlying BadgerDB instance is shut down correctly.

    Key configuration options:

    • Dir: The directory where the database files will be stored.
    • ValueDir: The directory where the actual values (data) will be stored.
    options := badgerhold.DefaultOptions
    options.Dir = "data"
    options.ValueDir = "data"
    
    store, err := badgerhold.Open(options)
    defer store.Close()
    if err != nil {
    	// handle error
    	log.Fatal(err)
    }
  7. Querying slice fields

    master

    When a struct field is a slice, you can use Contains, ContainsAll, or ContainsAny to check for membership.

    Important: The In operator requirement The In operator (and ContainsAll/ContainsAny) expects a slice of interface{} ([]interface{}). You cannot pass a typed slice (like []string) directly into these methods due to Go's type system. Use the badgerhold.Slice() helper to convert your typed slice into the required []interface{} format.

    // Querying a slice field
    // If Set is []string{"1", "2", "3"}
    // bh.Where("Set").Contains("1") is true
    
    // Using the In operator with a typed slice
    t := []string{"1", "2", "3", "4"}
    // This works by converting the slice to []interface{}
    where := badgerhold.Where("Id").In(badgerhold.Slice(t)...)
  8. Define indexes using struct tags

    master

    To create an index on a specific field in your Go struct, use the badgerhold:"index" struct tag. If you want to specify a custom name for the index, use the badgerholdIndex tag instead.

    // Standard index using the default tag
    type Person struct {
    	Name     string
    	Division string `badgerhold:"index"` // Creates an index for Division
    }
    
    // Custom index name
    type Person struct {
    	Name     string
    	Division string `badgerholdIndex:"IdxDivision"` // Creates an index named IdxDivision
    }
    type Person struct {
    	Name string
    	Division string `badgerhold:"index"` // Creates an index for Division
    }
    
    // alternate struct tag if you wish to specify the index name
    type Person struct {
    	Name string
    	Division string `badgerholdIndex:"IdxDivision"` // Creates an index named IdxDivision
    }
  9. Use AggregateResult to process query results

    master

    AggregateResult is used to access the results of an aggregate query. It provides methods to extract grouped values, retrieve the underlying records, and perform mathematical or comparative operations on a specific field.

    Key Methods

    • Group(result ...interface{}): Extracts the field values used for grouping. The result arguments must be pointers to variables capable of holding the group values.
    • Reduction(result interface{}): Retrieves the collection of records belonging to the aggregate group. The result argument must be a pointer to a slice.
    • Count() uint64: Returns the number of records in the aggregate grouping.
    • Sum(field string) float64: Returns the sum of the specified field. The field must be a numeric type (int, uint, or float).
    • Avg(field string) float64: Returns the average value of the specified field.
    • Min(field string, result interface{}): Finds the minimum value in the group based on the specified field and assigns it to result (which must be a non-nil pointer).
    • Max(field string, result interface{}): Finds the maximum value in the group based on the specified field and assigns it to result (which must be a non-nil pointer).
    • Sort(field string): Sorts the reduction by the specified field in ascending order. Note that the field name must start with an upper-case letter.
    // Example of using AggregateResult methods
    var groupVal string
    var records []MyDataType
    
    aggResult.Group(&groupVal)
    aggResult.Reduction(&records)
    
    count := aggResult.Count()
    sum := aggResult.Sum("Amount")
    avg := aggResult.Avg("Amount")
    
    var minRecord MyDataType
    aggResult.Min("Amount", &minRecord)
  10. Implement the Storer interface to optimize performance

    master

    By default, BadgerHold uses reflection to determine type names and indexes. To avoid the performance overhead of reflection, implement the Storer interface on your data types. This allows you to explicitly define the index prefix and the available indexes.

    type MyType struct {
    	ID string
    }
    
    // Implement Storer to skip reflection
    func (m MyType) Type() string {
    	return "MyType"
    }
    
    func (m MyType) Indexes() map[string]badgerhold.Index {
    	return map[string]badgerhold.Index{
    		"ID": {IndexFunc: func(name string, value interface{}) ([]byte, error) {
    			// Custom index logic
    			return nil, nil
    		}},
    	}
    }
  11. Enforce unique constraints on fields

    master

    You can ensure a field contains unique values across all records of a specific type by using the badgerhold:"unique" struct tag.

    If an Insert, Update, or Upsert operation attempts to violate this constraint, the operation will fail and return badgerhold.ErrUniqueExists.

    type User struct {
      Name string
      Email string `badgerhold:"unique"` // Only one User can have this Email
    }
  12. Build queries with the Query API

    master

    BadgerHold uses a chained API to build queries. You start a query using badgerhold.Where("FieldName") and then chain operators to define criteria. To add more conditions to the same field, use .And("AnotherField"). You can also use .Or(otherQuery) to union results from a separate query.

    Important: Field names passed to Where or And must start with an upper-case letter (e.g., "Name", not "name") because BadgerHold relies on exported fields for encoding. Using a lower-case field name will cause a panic.

    s.Find(badgerhold.Where("FieldName").Eq(value).And("AnotherField").Lt(AnotherValue).Or(badgerhold.Where("FieldName").Eq(anotherValue)))