- 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>
135 lines
5.6 KiB
Go
135 lines
5.6 KiB
Go
//go:build !(js && wasm)
|
|
|
|
package database
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// DatabaseConfig holds all database configuration options that can be passed
|
|
// to any database backend. Each backend uses the relevant fields for its type.
|
|
// This centralizes configuration instead of having each backend read env vars directly.
|
|
type DatabaseConfig struct {
|
|
// Common settings for all backends
|
|
DataDir string
|
|
LogLevel string
|
|
|
|
// Badger-specific settings
|
|
BlockCacheMB int // ORLY_DB_BLOCK_CACHE_MB
|
|
IndexCacheMB int // ORLY_DB_INDEX_CACHE_MB
|
|
QueryCacheDisabled bool // ORLY_QUERY_CACHE_DISABLED - disable query cache to reduce memory usage
|
|
QueryCacheSizeMB int // ORLY_QUERY_CACHE_SIZE_MB
|
|
QueryCacheMaxAge time.Duration // ORLY_QUERY_CACHE_MAX_AGE
|
|
|
|
// Serial cache settings for compact event storage
|
|
SerialCachePubkeys int // ORLY_SERIAL_CACHE_PUBKEYS - max pubkeys to cache (default: 100000)
|
|
SerialCacheEventIds int // ORLY_SERIAL_CACHE_EVENT_IDS - max event IDs to cache (default: 500000)
|
|
|
|
// Compression settings
|
|
ZSTDLevel int // ORLY_DB_ZSTD_LEVEL - ZSTD compression level (0=none, 1=fast, 3=default, 9=best)
|
|
|
|
// Neo4j-specific settings
|
|
Neo4jURI string // ORLY_NEO4J_URI
|
|
Neo4jUser string // ORLY_NEO4J_USER
|
|
Neo4jPassword string // ORLY_NEO4J_PASSWORD
|
|
|
|
// Neo4j driver tuning (memory and connection management)
|
|
Neo4jMaxConnPoolSize int // ORLY_NEO4J_MAX_CONN_POOL - max connection pool size (default: 25)
|
|
Neo4jFetchSize int // ORLY_NEO4J_FETCH_SIZE - max records per fetch batch (default: 1000)
|
|
Neo4jMaxTxRetrySeconds int // ORLY_NEO4J_MAX_TX_RETRY_SEC - max transaction retry time (default: 30)
|
|
Neo4jQueryResultLimit int // ORLY_NEO4J_QUERY_RESULT_LIMIT - max results per query (default: 10000, 0=unlimited)
|
|
|
|
// BBolt-specific settings (optimized for HDD)
|
|
BboltBatchMaxEvents int // ORLY_BBOLT_BATCH_MAX_EVENTS - max events per batch (default: 5000)
|
|
BboltBatchMaxBytes int64 // ORLY_BBOLT_BATCH_MAX_MB * 1024 * 1024 (default: 128MB)
|
|
BboltFlushTimeout time.Duration // ORLY_BBOLT_BATCH_FLUSH_SEC * time.Second (default: 30s)
|
|
BboltBloomSizeMB int // ORLY_BBOLT_BLOOM_SIZE_MB - bloom filter size (default: 16MB)
|
|
BboltNoSync bool // ORLY_BBOLT_NO_SYNC - disable fsync (DANGEROUS)
|
|
BboltMmapSize int // ORLY_BBOLT_MMAP_SIZE_GB * 1024 * 1024 * 1024 (default: 8GB)
|
|
}
|
|
|
|
// NewDatabase creates a database instance based on the specified type.
|
|
// Supported types: "badger", "neo4j"
|
|
func NewDatabase(
|
|
ctx context.Context,
|
|
cancel context.CancelFunc,
|
|
dbType string,
|
|
dataDir string,
|
|
logLevel string,
|
|
) (Database, error) {
|
|
// Create a default config for backward compatibility with existing callers
|
|
cfg := &DatabaseConfig{
|
|
DataDir: dataDir,
|
|
LogLevel: logLevel,
|
|
}
|
|
return NewDatabaseWithConfig(ctx, cancel, dbType, cfg)
|
|
}
|
|
|
|
// NewDatabaseWithConfig creates a database instance with full configuration.
|
|
// This is the preferred method when you have access to the app config.
|
|
func NewDatabaseWithConfig(
|
|
ctx context.Context,
|
|
cancel context.CancelFunc,
|
|
dbType string,
|
|
cfg *DatabaseConfig,
|
|
) (Database, error) {
|
|
switch strings.ToLower(dbType) {
|
|
case "badger", "":
|
|
// Use the existing badger implementation
|
|
return NewWithConfig(ctx, cancel, cfg)
|
|
case "neo4j":
|
|
// Use the neo4j implementation
|
|
if newNeo4jDatabase == nil {
|
|
return nil, fmt.Errorf("neo4j database backend not available (import _ \"next.orly.dev/pkg/neo4j\")")
|
|
}
|
|
return newNeo4jDatabase(ctx, cancel, cfg)
|
|
case "wasmdb", "indexeddb", "wasm":
|
|
// Use the wasmdb implementation (IndexedDB backend for WebAssembly)
|
|
if newWasmDBDatabase == nil {
|
|
return nil, fmt.Errorf("wasmdb database backend not available (import _ \"next.orly.dev/pkg/wasmdb\")")
|
|
}
|
|
return newWasmDBDatabase(ctx, cancel, cfg)
|
|
case "bbolt", "bolt":
|
|
// Use the bbolt implementation (B+tree, optimized for HDD)
|
|
if newBboltDatabase == nil {
|
|
return nil, fmt.Errorf("bbolt database backend not available (import _ \"next.orly.dev/pkg/bbolt\")")
|
|
}
|
|
return newBboltDatabase(ctx, cancel, cfg)
|
|
default:
|
|
return nil, fmt.Errorf("unsupported database type: %s (supported: badger, neo4j, wasmdb, bbolt)", dbType)
|
|
}
|
|
}
|
|
|
|
// newNeo4jDatabase creates a neo4j database instance
|
|
// This is defined here to avoid import cycles
|
|
var newNeo4jDatabase func(context.Context, context.CancelFunc, *DatabaseConfig) (Database, error)
|
|
|
|
// RegisterNeo4jFactory registers the neo4j database factory
|
|
// This is called from the neo4j package's init() function
|
|
func RegisterNeo4jFactory(factory func(context.Context, context.CancelFunc, *DatabaseConfig) (Database, error)) {
|
|
newNeo4jDatabase = factory
|
|
}
|
|
|
|
// newWasmDBDatabase creates a wasmdb database instance (IndexedDB backend for WebAssembly)
|
|
// This is defined here to avoid import cycles
|
|
var newWasmDBDatabase func(context.Context, context.CancelFunc, *DatabaseConfig) (Database, error)
|
|
|
|
// RegisterWasmDBFactory registers the wasmdb database factory
|
|
// This is called from the wasmdb package's init() function
|
|
func RegisterWasmDBFactory(factory func(context.Context, context.CancelFunc, *DatabaseConfig) (Database, error)) {
|
|
newWasmDBDatabase = factory
|
|
}
|
|
|
|
// newBboltDatabase creates a bbolt database instance
|
|
// This is defined here to avoid import cycles
|
|
var newBboltDatabase func(context.Context, context.CancelFunc, *DatabaseConfig) (Database, error)
|
|
|
|
// RegisterBboltFactory registers the bbolt database factory
|
|
// This is called from the bbolt package's init() function
|
|
func RegisterBboltFactory(factory func(context.Context, context.CancelFunc, *DatabaseConfig) (Database, error)) {
|
|
newBboltDatabase = factory
|
|
}
|