Skip to content

Commit

Permalink
feat(examples): add p/demo/avl/index
Browse files Browse the repository at this point in the history
Signed-off-by: moul <[email protected]>
  • Loading branch information
moul committed Dec 10, 2024
1 parent 5c31552 commit 887c1f0
Show file tree
Hide file tree
Showing 10 changed files with 821 additions and 2 deletions.
1 change: 1 addition & 0 deletions examples/gno.land/p/demo/avl/index/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/p/demo/avl/index
155 changes: 155 additions & 0 deletions examples/gno.land/p/demo/avl/index/index.gno
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
}
193 changes: 193 additions & 0 deletions examples/gno.land/p/demo/avl/index/index_test.gno
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")
}
})
}
4 changes: 2 additions & 2 deletions examples/gno.land/p/demo/avl/pager/pager.gno
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (

// Pager is a struct that holds the AVL tree and pagination parameters.
type Pager struct {
Tree *avl.Tree
Tree avl.ITree
PageQueryParam string
SizeQueryParam string
DefaultPageSize int
Expand All @@ -37,7 +37,7 @@ type Item struct {
}

// NewPager creates a new Pager with default values.
func NewPager(tree *avl.Tree, defaultPageSize int, reversed bool) *Pager {
func NewPager(tree avl.TreeInterface, defaultPageSize int, reversed bool) *Pager {
return &Pager{
Tree: tree,
PageQueryParam: "page",
Expand Down
1 change: 1 addition & 0 deletions examples/gno.land/p/demo/avl/rotree/gno.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module gno.land/p/demo/avl/rotree
Loading

0 comments on commit 887c1f0

Please sign in to comment.