- BBolt B+tree backend with sequential access patterns for spinning disks - Write batching (5000 events / 128MB / 30s flush) to reduce disk thrashing - Adjacency list storage for graph data (one key per vertex, not per edge) - Bloom filter for fast negative edge existence checks (~12MB for 10M edges) - No query cache (saves RAM, B+tree reads are fast enough on HDD) - Migration tool: orly migrate --from badger --to bbolt - Configuration: ORLY_BBOLT_* environment variables Files modified: - app/config/config.go: Added BBolt configuration options - main.go: Added migrate subcommand and BBolt config wiring - pkg/database/factory.go: Added BBolt factory registration - pkg/bbolt/*: New BBolt database backend implementation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
288 lines
6.9 KiB
Go
288 lines
6.9 KiB
Go
//go:build !(js && wasm)
|
|
|
|
package bbolt
|
|
|
|
import (
|
|
"bytes"
|
|
|
|
bolt "go.etcd.io/bbolt"
|
|
"next.orly.dev/pkg/database/indexes/types"
|
|
)
|
|
|
|
// EdgeExists checks if an edge exists between two serials.
|
|
// Uses bloom filter for fast negative lookups.
|
|
func (b *B) EdgeExists(srcSerial, dstSerial uint64, edgeType byte) (bool, error) {
|
|
// Fast path: check bloom filter first
|
|
if !b.edgeBloom.MayExist(srcSerial, dstSerial, edgeType) {
|
|
return false, nil // Definitely doesn't exist
|
|
}
|
|
|
|
// Bloom says maybe - need to verify in adjacency list
|
|
return b.verifyEdgeInAdjacencyList(srcSerial, dstSerial, edgeType)
|
|
}
|
|
|
|
// verifyEdgeInAdjacencyList checks the adjacency list for edge existence.
|
|
func (b *B) verifyEdgeInAdjacencyList(srcSerial, dstSerial uint64, edgeType byte) (bool, error) {
|
|
var exists bool
|
|
|
|
err := b.db.View(func(tx *bolt.Tx) error {
|
|
bucket := tx.Bucket(bucketEv)
|
|
if bucket == nil {
|
|
return nil
|
|
}
|
|
|
|
key := makeSerialKey(srcSerial)
|
|
data := bucket.Get(key)
|
|
if data == nil {
|
|
return nil
|
|
}
|
|
|
|
vertex := &EventVertex{}
|
|
if err := vertex.Decode(data); err != nil {
|
|
return err
|
|
}
|
|
|
|
switch edgeType {
|
|
case EdgeTypeAuthor:
|
|
exists = vertex.AuthorSerial == dstSerial
|
|
case EdgeTypePTag:
|
|
for _, s := range vertex.PTagSerials {
|
|
if s == dstSerial {
|
|
exists = true
|
|
break
|
|
}
|
|
}
|
|
case EdgeTypeETag:
|
|
for _, s := range vertex.ETagSerials {
|
|
if s == dstSerial {
|
|
exists = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
return exists, err
|
|
}
|
|
|
|
// GetEventVertex retrieves the adjacency list for an event.
|
|
func (b *B) GetEventVertex(eventSerial uint64) (*EventVertex, error) {
|
|
var vertex *EventVertex
|
|
|
|
err := b.db.View(func(tx *bolt.Tx) error {
|
|
bucket := tx.Bucket(bucketEv)
|
|
if bucket == nil {
|
|
return nil
|
|
}
|
|
|
|
key := makeSerialKey(eventSerial)
|
|
data := bucket.Get(key)
|
|
if data == nil {
|
|
return nil
|
|
}
|
|
|
|
vertex = &EventVertex{}
|
|
return vertex.Decode(data)
|
|
})
|
|
|
|
return vertex, err
|
|
}
|
|
|
|
// GetPubkeyVertex retrieves the adjacency list for a pubkey.
|
|
func (b *B) GetPubkeyVertex(pubkeySerial uint64) (*PubkeyVertex, error) {
|
|
var vertex *PubkeyVertex
|
|
|
|
err := b.db.View(func(tx *bolt.Tx) error {
|
|
bucket := tx.Bucket(bucketPv)
|
|
if bucket == nil {
|
|
return nil
|
|
}
|
|
|
|
key := makeSerialKey(pubkeySerial)
|
|
data := bucket.Get(key)
|
|
if data == nil {
|
|
return nil
|
|
}
|
|
|
|
vertex = &PubkeyVertex{}
|
|
return vertex.Decode(data)
|
|
})
|
|
|
|
return vertex, err
|
|
}
|
|
|
|
// GetEventsAuthoredBy returns event serials authored by a pubkey.
|
|
func (b *B) GetEventsAuthoredBy(pubkeySerial uint64) ([]uint64, error) {
|
|
vertex, err := b.GetPubkeyVertex(pubkeySerial)
|
|
if err != nil || vertex == nil {
|
|
return nil, err
|
|
}
|
|
return vertex.AuthoredEvents, nil
|
|
}
|
|
|
|
// GetEventsMentioning returns event serials that mention a pubkey.
|
|
func (b *B) GetEventsMentioning(pubkeySerial uint64) ([]uint64, error) {
|
|
vertex, err := b.GetPubkeyVertex(pubkeySerial)
|
|
if err != nil || vertex == nil {
|
|
return nil, err
|
|
}
|
|
return vertex.MentionedIn, nil
|
|
}
|
|
|
|
// GetPTagsFromEvent returns pubkey serials tagged in an event.
|
|
func (b *B) GetPTagsFromEvent(eventSerial uint64) ([]uint64, error) {
|
|
vertex, err := b.GetEventVertex(eventSerial)
|
|
if err != nil || vertex == nil {
|
|
return nil, err
|
|
}
|
|
return vertex.PTagSerials, nil
|
|
}
|
|
|
|
// GetETagsFromEvent returns event serials referenced by an event.
|
|
func (b *B) GetETagsFromEvent(eventSerial uint64) ([]uint64, error) {
|
|
vertex, err := b.GetEventVertex(eventSerial)
|
|
if err != nil || vertex == nil {
|
|
return nil, err
|
|
}
|
|
return vertex.ETagSerials, nil
|
|
}
|
|
|
|
// GetFollowsFromPubkeySerial returns the pubkey serials that a user follows.
|
|
// This extracts p-tags from the user's kind-3 contact list event.
|
|
func (b *B) GetFollowsFromPubkeySerial(pubkeySerial *types.Uint40) ([]*types.Uint40, error) {
|
|
if pubkeySerial == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
// Find the kind-3 event for this pubkey
|
|
contactEventSerial, err := b.FindEventByAuthorAndKind(pubkeySerial.Get(), 3)
|
|
if err != nil {
|
|
return nil, nil // No kind-3 event found is not an error
|
|
}
|
|
if contactEventSerial == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// Get the p-tags from the event vertex
|
|
pTagSerials, err := b.GetPTagsFromEvent(contactEventSerial)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Convert to types.Uint40
|
|
result := make([]*types.Uint40, 0, len(pTagSerials))
|
|
for _, s := range pTagSerials {
|
|
ser := new(types.Uint40)
|
|
ser.Set(s)
|
|
result = append(result, ser)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// FindEventByAuthorAndKind finds an event serial by author and kind.
|
|
// For replaceable events like kind-3, returns the most recent one.
|
|
func (b *B) FindEventByAuthorAndKind(authorSerial uint64, kindNum uint16) (uint64, error) {
|
|
var resultSerial uint64
|
|
|
|
err := b.db.View(func(tx *bolt.Tx) error {
|
|
// First, get events authored by this pubkey
|
|
pvBucket := tx.Bucket(bucketPv)
|
|
if pvBucket == nil {
|
|
return nil
|
|
}
|
|
|
|
pvKey := makeSerialKey(authorSerial)
|
|
pvData := pvBucket.Get(pvKey)
|
|
if pvData == nil {
|
|
return nil
|
|
}
|
|
|
|
vertex := &PubkeyVertex{}
|
|
if err := vertex.Decode(pvData); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Search through authored events for matching kind
|
|
evBucket := tx.Bucket(bucketEv)
|
|
if evBucket == nil {
|
|
return nil
|
|
}
|
|
|
|
var latestTs int64
|
|
for _, eventSerial := range vertex.AuthoredEvents {
|
|
evKey := makeSerialKey(eventSerial)
|
|
evData := evBucket.Get(evKey)
|
|
if evData == nil {
|
|
continue
|
|
}
|
|
|
|
evVertex := &EventVertex{}
|
|
if err := evVertex.Decode(evData); err != nil {
|
|
continue
|
|
}
|
|
|
|
if evVertex.Kind == kindNum {
|
|
// For replaceable events, we need to check timestamp
|
|
// Get event to compare timestamps
|
|
fpcBucket := tx.Bucket(bucketFpc)
|
|
if fpcBucket != nil {
|
|
// Scan for matching serial prefix in fpc bucket
|
|
c := fpcBucket.Cursor()
|
|
prefix := makeSerialKey(eventSerial)
|
|
for k, _ := c.Seek(prefix); k != nil && bytes.HasPrefix(k, prefix); k, _ = c.Next() {
|
|
// Key format: serial(5) | id(32) | pubkey_hash(8) | created_at(8)
|
|
if len(k) >= 53 {
|
|
ts := int64(decodeUint64(k[45:53]))
|
|
if ts > latestTs {
|
|
latestTs = ts
|
|
resultSerial = eventSerial
|
|
}
|
|
}
|
|
break
|
|
}
|
|
} else {
|
|
// If no fpc bucket, just take the first match
|
|
resultSerial = eventSerial
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
return resultSerial, err
|
|
}
|
|
|
|
// GetReferencingEvents returns event serials that reference a target event via e-tag.
|
|
func (b *B) GetReferencingEvents(targetSerial uint64) ([]uint64, error) {
|
|
var result []uint64
|
|
|
|
err := b.db.View(func(tx *bolt.Tx) error {
|
|
bucket := tx.Bucket(bucketEv)
|
|
if bucket == nil {
|
|
return nil
|
|
}
|
|
|
|
// Scan all event vertices looking for e-tag references
|
|
// Note: This is O(n) - for production, consider a reverse index
|
|
c := bucket.Cursor()
|
|
for k, v := c.First(); k != nil; k, v = c.Next() {
|
|
vertex := &EventVertex{}
|
|
if err := vertex.Decode(v); err != nil {
|
|
continue
|
|
}
|
|
|
|
for _, eTagSerial := range vertex.ETagSerials {
|
|
if eTagSerial == targetSerial {
|
|
eventSerial := decodeUint40(k)
|
|
result = append(result, eventSerial)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
return result, err
|
|
}
|