...
This commit is contained in:
616
pkg/data/radixtree/radixtree.go
Normal file
616
pkg/data/radixtree/radixtree.go
Normal file
@@ -0,0 +1,616 @@
|
||||
// Package radixtree provides a persistent radix tree implementation using the ourdb package for storage
|
||||
package radixtree
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/freeflowuniverse/heroagent/pkg/data/ourdb"
|
||||
)
|
||||
|
||||
// Node represents a node in the radix tree
|
||||
type Node struct {
|
||||
KeySegment string // The segment of the key stored at this node
|
||||
Value []byte // Value stored at this node (empty if not a leaf)
|
||||
Children []NodeRef // References to child nodes
|
||||
IsLeaf bool // Whether this node is a leaf node
|
||||
}
|
||||
|
||||
// NodeRef is a reference to a node in the database
|
||||
type NodeRef struct {
|
||||
KeyPart string // The key segment for this child
|
||||
NodeID uint32 // Database ID of the node
|
||||
}
|
||||
|
||||
// RadixTree represents a radix tree data structure
|
||||
type RadixTree struct {
|
||||
DB *ourdb.OurDB // Database for persistent storage
|
||||
RootID uint32 // Database ID of the root node
|
||||
}
|
||||
|
||||
// NewArgs contains arguments for creating a new RadixTree
|
||||
type NewArgs struct {
|
||||
Path string // Path to the database
|
||||
Reset bool // Whether to reset the database
|
||||
}
|
||||
|
||||
// New creates a new radix tree with the specified database path
|
||||
func New(args NewArgs) (*RadixTree, error) {
|
||||
config := ourdb.DefaultConfig()
|
||||
config.Path = args.Path
|
||||
config.RecordSizeMax = 1024 * 4 // 4KB max record size
|
||||
config.IncrementalMode = true
|
||||
config.Reset = args.Reset
|
||||
|
||||
db, err := ourdb.New(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var rootID uint32 = 1 // First ID in ourdb is 1
|
||||
nextID, err := db.GetNextID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if nextID == 1 {
|
||||
// Create new root node
|
||||
root := Node{
|
||||
KeySegment: "",
|
||||
Value: []byte{},
|
||||
Children: []NodeRef{},
|
||||
IsLeaf: false,
|
||||
}
|
||||
rootData := serializeNode(root)
|
||||
rootID, err = db.Set(ourdb.OurDBSetArgs{
|
||||
Data: rootData,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rootID != 1 {
|
||||
return nil, errors.New("expected root ID to be 1")
|
||||
}
|
||||
} else {
|
||||
// Use existing root node
|
||||
_, err := db.Get(1) // Verify root node exists
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &RadixTree{
|
||||
DB: db,
|
||||
RootID: rootID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Set sets a key-value pair in the tree
|
||||
func (rt *RadixTree) Set(key string, value []byte) error {
|
||||
currentID := rt.RootID
|
||||
offset := 0
|
||||
|
||||
// Handle empty key case
|
||||
if len(key) == 0 {
|
||||
rootData, err := rt.DB.Get(currentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rootNode, err := deserializeNode(rootData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rootNode.IsLeaf = true
|
||||
rootNode.Value = value
|
||||
_, err = rt.DB.Set(ourdb.OurDBSetArgs{
|
||||
ID: ¤tID,
|
||||
Data: serializeNode(rootNode),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
for offset < len(key) {
|
||||
nodeData, err := rt.DB.Get(currentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
node, err := deserializeNode(nodeData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Find matching child
|
||||
matchedChild := -1
|
||||
for i, child := range node.Children {
|
||||
if hasPrefix(key[offset:], child.KeyPart) {
|
||||
matchedChild = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if matchedChild == -1 {
|
||||
// No matching child found, create new leaf node
|
||||
keyPart := key[offset:]
|
||||
newNode := Node{
|
||||
KeySegment: keyPart,
|
||||
Value: value,
|
||||
Children: []NodeRef{},
|
||||
IsLeaf: true,
|
||||
}
|
||||
newID, err := rt.DB.Set(ourdb.OurDBSetArgs{
|
||||
Data: serializeNode(newNode),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create new child reference and update parent node
|
||||
node.Children = append(node.Children, NodeRef{
|
||||
KeyPart: keyPart,
|
||||
NodeID: newID,
|
||||
})
|
||||
|
||||
// Update parent node in DB
|
||||
_, err = rt.DB.Set(ourdb.OurDBSetArgs{
|
||||
ID: ¤tID,
|
||||
Data: serializeNode(node),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
child := node.Children[matchedChild]
|
||||
commonPrefix := getCommonPrefix(key[offset:], child.KeyPart)
|
||||
|
||||
if len(commonPrefix) < len(child.KeyPart) {
|
||||
// Split existing node
|
||||
childData, err := rt.DB.Get(child.NodeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
childNode, err := deserializeNode(childData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create new intermediate node
|
||||
newNode := Node{
|
||||
KeySegment: child.KeyPart[len(commonPrefix):],
|
||||
Value: childNode.Value,
|
||||
Children: childNode.Children,
|
||||
IsLeaf: childNode.IsLeaf,
|
||||
}
|
||||
newID, err := rt.DB.Set(ourdb.OurDBSetArgs{
|
||||
Data: serializeNode(newNode),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Update current node
|
||||
node.Children[matchedChild] = NodeRef{
|
||||
KeyPart: commonPrefix,
|
||||
NodeID: newID,
|
||||
}
|
||||
_, err = rt.DB.Set(ourdb.OurDBSetArgs{
|
||||
ID: ¤tID,
|
||||
Data: serializeNode(node),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if offset+len(commonPrefix) == len(key) {
|
||||
// Update value at existing node
|
||||
childData, err := rt.DB.Get(child.NodeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
childNode, err := deserializeNode(childData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
childNode.Value = value
|
||||
childNode.IsLeaf = true
|
||||
_, err = rt.DB.Set(ourdb.OurDBSetArgs{
|
||||
ID: &child.NodeID,
|
||||
Data: serializeNode(childNode),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
offset += len(commonPrefix)
|
||||
currentID = child.NodeID
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get retrieves a value by key from the tree
|
||||
func (rt *RadixTree) Get(key string) ([]byte, error) {
|
||||
currentID := rt.RootID
|
||||
offset := 0
|
||||
|
||||
// Handle empty key case
|
||||
if len(key) == 0 {
|
||||
rootData, err := rt.DB.Get(currentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rootNode, err := deserializeNode(rootData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rootNode.IsLeaf {
|
||||
return rootNode.Value, nil
|
||||
}
|
||||
return nil, errors.New("key not found")
|
||||
}
|
||||
|
||||
for offset < len(key) {
|
||||
nodeData, err := rt.DB.Get(currentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
node, err := deserializeNode(nodeData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, child := range node.Children {
|
||||
if hasPrefix(key[offset:], child.KeyPart) {
|
||||
if offset+len(child.KeyPart) == len(key) {
|
||||
childData, err := rt.DB.Get(child.NodeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
childNode, err := deserializeNode(childData)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if childNode.IsLeaf {
|
||||
return childNode.Value, nil
|
||||
}
|
||||
}
|
||||
currentID = child.NodeID
|
||||
offset += len(child.KeyPart)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return nil, errors.New("key not found")
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New("key not found")
|
||||
}
|
||||
|
||||
// Update updates the value at a given key prefix, preserving the prefix while replacing the remainder
|
||||
func (rt *RadixTree) Update(prefix string, newValue []byte) error {
|
||||
currentID := rt.RootID
|
||||
offset := 0
|
||||
|
||||
// Handle empty prefix case
|
||||
if len(prefix) == 0 {
|
||||
return errors.New("empty prefix not allowed")
|
||||
}
|
||||
|
||||
for offset < len(prefix) {
|
||||
nodeData, err := rt.DB.Get(currentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
node, err := deserializeNode(nodeData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, child := range node.Children {
|
||||
if hasPrefix(prefix[offset:], child.KeyPart) {
|
||||
if offset+len(child.KeyPart) == len(prefix) {
|
||||
// Found exact prefix match
|
||||
childData, err := rt.DB.Get(child.NodeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
childNode, err := deserializeNode(childData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if childNode.IsLeaf {
|
||||
// Update the value
|
||||
childNode.Value = newValue
|
||||
_, err = rt.DB.Set(ourdb.OurDBSetArgs{
|
||||
ID: &child.NodeID,
|
||||
Data: serializeNode(childNode),
|
||||
})
|
||||
return err
|
||||
}
|
||||
}
|
||||
currentID = child.NodeID
|
||||
offset += len(child.KeyPart)
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return errors.New("prefix not found")
|
||||
}
|
||||
}
|
||||
|
||||
return errors.New("prefix not found")
|
||||
}
|
||||
|
||||
// Delete deletes a key from the tree
|
||||
func (rt *RadixTree) Delete(key string) error {
|
||||
currentID := rt.RootID
|
||||
offset := 0
|
||||
var path []NodeRef
|
||||
|
||||
// Find the node to delete
|
||||
for offset < len(key) {
|
||||
nodeData, err := rt.DB.Get(currentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
node, err := deserializeNode(nodeData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, child := range node.Children {
|
||||
if hasPrefix(key[offset:], child.KeyPart) {
|
||||
path = append(path, child)
|
||||
currentID = child.NodeID
|
||||
offset += len(child.KeyPart)
|
||||
found = true
|
||||
|
||||
// Check if we've matched the full key
|
||||
if offset == len(key) {
|
||||
childData, err := rt.DB.Get(child.NodeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
childNode, err := deserializeNode(childData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if childNode.IsLeaf {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return errors.New("key not found")
|
||||
}
|
||||
}
|
||||
|
||||
if len(path) == 0 {
|
||||
return errors.New("key not found")
|
||||
}
|
||||
|
||||
// Get the node to delete
|
||||
lastNodeID := path[len(path)-1].NodeID
|
||||
lastNodeData, err := rt.DB.Get(lastNodeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lastNode, err := deserializeNode(lastNodeData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the node has children, just mark it as non-leaf
|
||||
if len(lastNode.Children) > 0 {
|
||||
lastNode.IsLeaf = false
|
||||
lastNode.Value = []byte{}
|
||||
_, err = rt.DB.Set(ourdb.OurDBSetArgs{
|
||||
ID: &lastNodeID,
|
||||
Data: serializeNode(lastNode),
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// If node has no children, remove it from parent
|
||||
if len(path) > 1 {
|
||||
parentNodeID := path[len(path)-2].NodeID
|
||||
parentNodeData, err := rt.DB.Get(parentNodeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parentNode, err := deserializeNode(parentNodeData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove child from parent
|
||||
for i, child := range parentNode.Children {
|
||||
if child.NodeID == lastNodeID {
|
||||
// Remove child at index i
|
||||
parentNode.Children = append(parentNode.Children[:i], parentNode.Children[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
_, err = rt.DB.Set(ourdb.OurDBSetArgs{
|
||||
ID: &parentNodeID,
|
||||
Data: serializeNode(parentNode),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete the node from the database
|
||||
return rt.DB.Delete(lastNodeID)
|
||||
} else {
|
||||
// If this is a direct child of the root, just mark it as non-leaf
|
||||
lastNode.IsLeaf = false
|
||||
lastNode.Value = []byte{}
|
||||
_, err = rt.DB.Set(ourdb.OurDBSetArgs{
|
||||
ID: &lastNodeID,
|
||||
Data: serializeNode(lastNode),
|
||||
})
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// List lists all keys with a given prefix
|
||||
func (rt *RadixTree) List(prefix string) ([]string, error) {
|
||||
result := []string{}
|
||||
|
||||
// Handle empty prefix case - will return all keys
|
||||
if len(prefix) == 0 {
|
||||
err := rt.collectAllKeys(rt.RootID, "", &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Start from the root and find all matching keys
|
||||
err := rt.findKeysWithPrefix(rt.RootID, "", prefix, &result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Helper function to find all keys with a given prefix
|
||||
func (rt *RadixTree) findKeysWithPrefix(nodeID uint32, currentPath, prefix string, result *[]string) error {
|
||||
nodeData, err := rt.DB.Get(nodeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
node, err := deserializeNode(nodeData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the current path already matches or exceeds the prefix length
|
||||
if len(currentPath) >= len(prefix) {
|
||||
// Check if the current path starts with the prefix
|
||||
if hasPrefix(currentPath, prefix) {
|
||||
// If this is a leaf node, add it to the results
|
||||
if node.IsLeaf {
|
||||
*result = append(*result, currentPath)
|
||||
}
|
||||
|
||||
// Collect all keys from this subtree
|
||||
for _, child := range node.Children {
|
||||
childPath := currentPath + child.KeyPart
|
||||
err := rt.findKeysWithPrefix(child.NodeID, childPath, prefix, result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Current path is shorter than the prefix, continue searching
|
||||
for _, child := range node.Children {
|
||||
childPath := currentPath + child.KeyPart
|
||||
|
||||
// Check if this child's path could potentially match the prefix
|
||||
if hasPrefix(prefix, currentPath) {
|
||||
// The prefix starts with the current path, so we need to check if
|
||||
// the child's key_part matches the next part of the prefix
|
||||
prefixRemainder := prefix[len(currentPath):]
|
||||
|
||||
// If the prefix remainder starts with the child's key_part or vice versa
|
||||
if hasPrefix(prefixRemainder, child.KeyPart) ||
|
||||
(hasPrefix(child.KeyPart, prefixRemainder) && len(child.KeyPart) >= len(prefixRemainder)) {
|
||||
err := rt.findKeysWithPrefix(child.NodeID, childPath, prefix, result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper function to recursively collect all keys under a node
|
||||
func (rt *RadixTree) collectAllKeys(nodeID uint32, currentPath string, result *[]string) error {
|
||||
nodeData, err := rt.DB.Get(nodeID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
node, err := deserializeNode(nodeData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If this node is a leaf, add its path to the result
|
||||
if node.IsLeaf {
|
||||
*result = append(*result, currentPath)
|
||||
}
|
||||
|
||||
// Recursively collect keys from all children
|
||||
for _, child := range node.Children {
|
||||
childPath := currentPath + child.KeyPart
|
||||
err := rt.collectAllKeys(child.NodeID, childPath, result)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAll gets all values for keys with a given prefix
|
||||
func (rt *RadixTree) GetAll(prefix string) ([][]byte, error) {
|
||||
// Get all matching keys
|
||||
keys, err := rt.List(prefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get values for each key
|
||||
values := [][]byte{}
|
||||
for _, key := range keys {
|
||||
value, err := rt.Get(key)
|
||||
if err == nil {
|
||||
values = append(values, value)
|
||||
}
|
||||
}
|
||||
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// Close closes the database
|
||||
func (rt *RadixTree) Close() error {
|
||||
return rt.DB.Close()
|
||||
}
|
||||
|
||||
// Destroy closes and removes the database
|
||||
func (rt *RadixTree) Destroy() error {
|
||||
return rt.DB.Destroy()
|
||||
}
|
||||
|
||||
// Helper function to get the common prefix of two strings
|
||||
func getCommonPrefix(a, b string) string {
|
||||
i := 0
|
||||
for i < len(a) && i < len(b) && a[i] == b[i] {
|
||||
i++
|
||||
}
|
||||
return a[:i]
|
||||
}
|
||||
|
||||
// Helper function to check if a string has a prefix
|
||||
func hasPrefix(s, prefix string) bool {
|
||||
if len(s) < len(prefix) {
|
||||
return false
|
||||
}
|
||||
return s[:len(prefix)] == prefix
|
||||
}
|
||||
Reference in New Issue
Block a user