-
Notifications
You must be signed in to change notification settings - Fork 397
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(examples): add p/demo/avl/index
Signed-off-by: moul <[email protected]>
- Loading branch information
Showing
10 changed files
with
821 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module gno.land/p/demo/avl/index |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
package index | ||
|
||
import ( | ||
"gno.land/p/demo/avl" | ||
) | ||
|
||
// KeyExtractor is a function that extracts an index key from a value | ||
type KeyExtractor func(value interface{}) string | ||
|
||
// IndexedTree wraps an AVL tree and maintains secondary indexes | ||
type IndexedTree struct { | ||
primary *avl.Tree | ||
indexes map[string]*Index | ||
} | ||
|
||
// Index represents a secondary index | ||
type Index struct { | ||
tree *avl.Tree | ||
extract KeyExtractor | ||
} | ||
|
||
// NewIndexedTree creates a new indexed tree wrapper | ||
func NewIndexedTree() *IndexedTree { | ||
return &IndexedTree{ | ||
primary: avl.NewTree(), | ||
indexes: make(map[string]*Index), | ||
} | ||
} | ||
|
||
// AddIndex creates a new secondary index with the given name and key extractor | ||
func (it *IndexedTree) AddIndex(name string, extract KeyExtractor) { | ||
it.indexes[name] = &Index{ | ||
tree: avl.NewTree(), | ||
extract: extract, | ||
} | ||
} | ||
|
||
// Set adds or updates a value in the primary tree and all indexes | ||
func (it *IndexedTree) Set(key string, value interface{}) bool { | ||
// First, if this is an update, we need to remove old index entries | ||
if oldValue, exists := it.primary.Get(key); exists { | ||
it.removeFromIndexes(key, oldValue) | ||
} | ||
|
||
// Update primary tree | ||
updated := it.primary.Set(key, value) | ||
|
||
// Update all indexes | ||
it.addToIndexes(key, value) | ||
|
||
return updated | ||
} | ||
|
||
// Remove removes a value from the primary tree and all indexes | ||
func (it *IndexedTree) Remove(key string) (interface{}, bool) { | ||
// Get the value first so we can remove it from indexes | ||
value, exists := it.primary.Get(key) | ||
if !exists { | ||
return nil, false | ||
} | ||
|
||
// Remove from indexes first | ||
it.removeFromIndexes(key, value) | ||
|
||
// Remove from primary tree | ||
return it.primary.Remove(key) | ||
} | ||
|
||
// GetByIndex retrieves values from a secondary index | ||
func (it *IndexedTree) GetByIndex(indexName string, indexKey string) []interface{} { | ||
index, exists := it.indexes[indexName] | ||
if !exists { | ||
return nil | ||
} | ||
|
||
var results []interface{} | ||
|
||
// Get all primary keys that match the index key | ||
value, exists := index.tree.Get(indexKey) | ||
if exists { | ||
primaryKeys := value.([]string) | ||
for _, primaryKey := range primaryKeys { | ||
if primaryValue, exists := it.primary.Get(primaryKey); exists { | ||
results = append(results, primaryValue) | ||
} | ||
} | ||
} | ||
|
||
return results | ||
} | ||
|
||
// Internal helper methods | ||
|
||
func (it *IndexedTree) addToIndexes(primaryKey string, value interface{}) { | ||
for _, index := range it.indexes { | ||
indexKey := index.extract(value) | ||
|
||
// Get existing keys or create new slice | ||
var keys []string | ||
if existing, exists := index.tree.Get(indexKey); exists { | ||
keys = existing.([]string) | ||
} | ||
keys = append(keys, primaryKey) | ||
|
||
index.tree.Set(indexKey, keys) | ||
} | ||
} | ||
|
||
func (it *IndexedTree) removeFromIndexes(primaryKey string, value interface{}) { | ||
for _, index := range it.indexes { | ||
indexKey := index.extract(value) | ||
|
||
// Get existing keys | ||
if existing, exists := index.tree.Get(indexKey); exists { | ||
keys := existing.([]string) | ||
// Remove the primary key from the slice | ||
newKeys := make([]string, 0) | ||
for _, k := range keys { | ||
if k != primaryKey { | ||
newKeys = append(newKeys, k) | ||
} | ||
} | ||
// If there are still keys, update the index, otherwise remove it | ||
if len(newKeys) > 0 { | ||
index.tree.Set(indexKey, newKeys) | ||
} else { | ||
index.tree.Remove(indexKey) | ||
} | ||
} | ||
} | ||
} | ||
|
||
func (it *IndexedTree) GetIndexTree(name string) avl.TreeInterface { | ||
if idx, exists := it.indexes[name]; exists { | ||
return idx.tree | ||
} | ||
return nil | ||
} | ||
|
||
func (it *IndexedTree) GetPrimary() avl.TreeInterface { | ||
return it.primary | ||
} | ||
|
||
func (it *IndexedTree) Update(key string, oldValue interface{}, newValue interface{}) bool { | ||
// Remove old value from indexes | ||
it.removeFromIndexes(key, oldValue) | ||
|
||
// Update primary tree | ||
updated := it.primary.Set(key, newValue) | ||
|
||
// Add new value to indexes | ||
it.addToIndexes(key, newValue) | ||
|
||
return updated | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
package index | ||
|
||
import ( | ||
"strconv" | ||
"testing" | ||
) | ||
|
||
type Person struct { | ||
ID string | ||
Name string | ||
Age int | ||
} | ||
|
||
type InvalidPerson struct { | ||
ID string | ||
} | ||
|
||
func TestIndexedTreeComprehensive(t *testing.T) { | ||
// Test 1: Basic operations without any indexes | ||
t.Run("NoIndexes", func(t *testing.T) { | ||
tree := NewIndexedTree() | ||
p1 := &Person{ID: "1", Name: "Alice", Age: 30} | ||
|
||
// Test Set and Get using primary tree directly | ||
tree.Set("1", p1) | ||
val, exists := tree.GetPrimary().Get("1") | ||
if !exists || val.(*Person).Name != "Alice" { | ||
t.Error("Basic Get failed without indexes") | ||
} | ||
|
||
// Test direct tree iteration | ||
count := 0 | ||
tree.GetPrimary().Iterate("", "", func(key string, value interface{}) bool { | ||
count++ | ||
return false | ||
}) | ||
if count != 1 { | ||
t.Error("Basic iteration failed") | ||
} | ||
}) | ||
|
||
// Test 2: Multiple indexes on same field | ||
t.Run("DuplicateIndexes", func(t *testing.T) { | ||
tree := NewIndexedTree() | ||
tree.AddIndex("age1", func(v interface{}) string { | ||
return strconv.Itoa(v.(*Person).Age) | ||
}) | ||
tree.AddIndex("age2", func(v interface{}) string { | ||
return strconv.Itoa(v.(*Person).Age) | ||
}) | ||
|
||
p1 := &Person{ID: "1", Name: "Alice", Age: 30} | ||
p2 := &Person{ID: "2", Name: "Bob", Age: 30} | ||
|
||
tree.Set("1", p1) | ||
tree.Set("2", p2) | ||
|
||
// Both indexes should return the same results | ||
results1 := tree.GetByIndex("age1", "30") | ||
results2 := tree.GetByIndex("age2", "30") | ||
|
||
if len(results1) != 2 || len(results2) != 2 { | ||
t.Error("Duplicate indexes returned different results") | ||
} | ||
}) | ||
|
||
// Test 3: Invalid extractor | ||
t.Run("InvalidExtractor", func(t *testing.T) { | ||
didPanic := false | ||
|
||
func() { | ||
defer func() { | ||
if r := recover(); r != nil { | ||
didPanic = true | ||
} | ||
}() | ||
|
||
tree := NewIndexedTree() | ||
tree.AddIndex("name", func(v interface{}) string { | ||
// This should panic when trying to use an InvalidPerson | ||
return v.(*Person).Name // Intentionally wrong type assertion | ||
}) | ||
|
||
invalid := &InvalidPerson{ID: "1"} | ||
tree.Set("1", invalid) // This should trigger the panic | ||
}() | ||
|
||
if !didPanic { | ||
t.Error("Expected panic from invalid type") | ||
} | ||
}) | ||
|
||
// Test 4: Mixed usage of indexed and direct access | ||
t.Run("MixedUsage", func(t *testing.T) { | ||
tree := NewIndexedTree() | ||
tree.AddIndex("age", func(v interface{}) string { | ||
return strconv.Itoa(v.(*Person).Age) | ||
}) | ||
|
||
p1 := &Person{ID: "1", Name: "Alice", Age: 30} | ||
|
||
// Use Set instead of direct tree access to ensure indexes are updated | ||
tree.Set("1", p1) | ||
|
||
// Index should work | ||
results := tree.GetByIndex("age", "30") | ||
if len(results) != 1 { | ||
t.Error("Index failed after direct tree usage") | ||
} | ||
}) | ||
|
||
// Test 5: Using index as TreeInterface | ||
t.Run("IndexAsTreeInterface", func(t *testing.T) { | ||
tree := NewIndexedTree() | ||
tree.AddIndex("age", func(v interface{}) string { | ||
return strconv.Itoa(v.(*Person).Age) | ||
}) | ||
|
||
p1 := &Person{ID: "1", Name: "Alice", Age: 30} | ||
p2 := &Person{ID: "2", Name: "Bob", Age: 30} | ||
|
||
tree.Set("1", p1) | ||
tree.Set("2", p2) | ||
|
||
// Get the index as TreeInterface | ||
ageIndex := tree.GetIndexTree("age") | ||
if ageIndex == nil { | ||
t.Error("Failed to get index as TreeInterface") | ||
} | ||
|
||
// Use the interface methods | ||
val, exists := ageIndex.Get("30") | ||
if !exists { | ||
t.Error("Failed to get value through index interface") | ||
} | ||
|
||
// The value should be a []string of primary keys | ||
primaryKeys := val.([]string) | ||
if len(primaryKeys) != 2 { | ||
t.Error("Wrong number of primary keys in index") | ||
} | ||
}) | ||
|
||
// Test 6: Remove operations | ||
t.Run("RemoveOperations", func(t *testing.T) { | ||
tree := NewIndexedTree() | ||
tree.AddIndex("age", func(v interface{}) string { | ||
return strconv.Itoa(v.(*Person).Age) | ||
}) | ||
|
||
p1 := &Person{ID: "1", Name: "Alice", Age: 30} | ||
tree.Set("1", p1) | ||
|
||
// Remove and verify both primary and index | ||
tree.Remove("1") | ||
|
||
if _, exists := tree.GetPrimary().Get("1"); exists { | ||
t.Error("Entry still exists in primary after remove") | ||
} | ||
|
||
results := tree.GetByIndex("age", "30") | ||
if len(results) != 0 { | ||
t.Error("Entry still exists in index after remove") | ||
} | ||
}) | ||
|
||
// Test 7: Update operations | ||
t.Run("UpdateOperations", func(t *testing.T) { | ||
tree := NewIndexedTree() | ||
tree.AddIndex("age", func(v interface{}) string { | ||
return strconv.Itoa(v.(*Person).Age) | ||
}) | ||
|
||
p1 := &Person{ID: "1", Name: "Alice", Age: 30} | ||
tree.Set("1", p1) | ||
|
||
// Update age using the new Update method | ||
p1New := &Person{ID: "1", Name: "Alice", Age: 31} | ||
tree.Update("1", p1, p1New) | ||
|
||
// Check old index is removed | ||
results30 := tree.GetByIndex("age", "30") | ||
if len(results30) != 0 { | ||
t.Error("Old index entry still exists") | ||
} | ||
|
||
// Check new index is added | ||
results31 := tree.GetByIndex("age", "31") | ||
if len(results31) != 1 { | ||
t.Error("New index entry not found") | ||
} | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
module gno.land/p/demo/avl/rotree |
Oops, something went wrong.