Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

collection.gno

13.21 Kb · 527 lines
  1// Package collection provides a generic collection implementation with support for
  2// multiple indexes, including unique indexes and case-insensitive indexes.
  3// It is designed to be used with any type and allows efficient lookups using
  4// different fields or computed values.
  5//
  6// Example usage:
  7//
  8//	// Define a data type
  9//	type User struct {
 10//	    Name     string
 11//	    Email    string
 12//	    Age      int
 13//	    Username string
 14//	    Tags     []string
 15//	}
 16//
 17//	// Create a new collection
 18//	c := collection.New()
 19//
 20//	// Add indexes with different options
 21//	c.AddIndex("name", func(v any) string {
 22//	    return v.(*User).Name
 23//	}, UniqueIndex)
 24//
 25//	c.AddIndex("email", func(v any) string {
 26//	    return v.(*User).Email
 27//	}, UniqueIndex|CaseInsensitiveIndex)
 28//
 29//	c.AddIndex("age", func(v any) string {
 30//	    return strconv.Itoa(v.(*User).Age)
 31//	}, DefaultIndex)  // Non-unique index
 32//
 33//	c.AddIndex("username", func(v any) string {
 34//	    return v.(*User).Username
 35//	}, UniqueIndex|SparseIndex)  // Allow empty usernames
 36//
 37//	// For tags, we index all tags for the user
 38//	c.AddIndex("tag", func(v any) []string {
 39//	    return v.(*User).Tags
 40//	}, DefaultIndex)  // Non-unique to allow multiple users with same tag
 41//
 42//	// Store an object
 43//	id := c.Set(&User{
 44//	    Name:  "Alice",
 45//	    Email: "alice@example.com",
 46//	    Age:   30,
 47//	    Tags:  []string{"admin", "moderator"},  // User can have multiple tags
 48//	})
 49//
 50//	// Retrieve by any index
 51//	entry := c.GetFirst("email", "alice@example.com")
 52//	adminUsers := c.GetAll("tag", "admin")      // Find all users with admin tag
 53//	modUsers := c.GetAll("tag", "moderator")    // Find all users with moderator tag
 54//
 55// Index options can be combined using the bitwise OR operator.
 56// Available options:
 57//   - DefaultIndex: Regular index with no special behavior
 58//   - UniqueIndex: Ensures values are unique within the index
 59//   - CaseInsensitiveIndex: Makes string comparisons case-insensitive
 60//   - SparseIndex: Skips indexing empty values (nil or empty string)
 61//
 62// Example: UniqueIndex|CaseInsensitiveIndex for a case-insensitive unique index
 63//
 64// # Versioning
 65//
 66// This is the B+ tree successor to [gno.land/p/moul/collection/v0] (which is
 67// backed by an AVL tree): a bump to v3 because the backing data structure — and
 68// thus the on-chain storage layout — changed. The exported API is unchanged from
 69// v2; only the persisted representation differs, so it is a compatibility (not a
 70// source) change. A B+ tree packs many entries per persisted node, so each index
 71// entry costs materially less storage and gas than the AVL backing; prefer v3
 72// when the collection is part of persisted realm state.
 73//
 74// Two operational caveats inherited from the in-place-mutating B+ tree backing:
 75//
 76//   - do NOT mutate the collection (Set/Update/Delete) from inside an index
 77//     iteration callback — the AVL backing's copy-on-write tolerated it, this one
 78//     does not;
 79//   - do NOT copy a non-zero Collection by value — the copies would share live
 80//     tree nodes while their state diverges.
 81package collection
 82
 83import (
 84	"errors"
 85	"strings"
 86
 87	"gno.land/p/nt/bptree/v0"
 88	"gno.land/p/nt/seqid/v0"
 89)
 90
 91// New creates a new Collection instance with an initialized ID index.
 92// The ID index is a special unique index that is always present and
 93// serves as the primary key for all objects in the collection.
 94func New() *Collection {
 95	c := &Collection{
 96		indexes: make(map[string]*Index),
 97		idGen:   seqid.ID(0),
 98	}
 99	// Initialize _id index
100	c.indexes[IDIndex] = &Index{
101		options: UniqueIndex,
102		tree:    bptree.NewBPTree32(),
103	}
104	return c
105}
106
107// Collection represents a collection of objects with multiple indexes
108type Collection struct {
109	indexes map[string]*Index
110	idGen   seqid.ID
111}
112
113const (
114	// IDIndex is the reserved name for the primary key index
115	IDIndex = "_id"
116)
117
118// IndexOption represents configuration options for an index using bit flags
119type IndexOption uint64
120
121const (
122	// DefaultIndex is a basic index with no special options
123	DefaultIndex IndexOption = 0
124
125	// UniqueIndex ensures no duplicate values are allowed
126	UniqueIndex IndexOption = 1 << iota
127
128	// CaseInsensitiveIndex automatically converts string values to lowercase
129	CaseInsensitiveIndex
130
131	// SparseIndex only indexes non-empty values
132	SparseIndex
133)
134
135// Index represents an index with its configuration and data.
136// The index function can return either:
137//   - string: for single-value indexes
138//   - []string: for multi-value indexes where one object can be indexed under multiple keys
139//
140// The backing tree stores either a single ID or []string for multiple IDs per key.
141type Index struct {
142	fn      any
143	options IndexOption
144	tree    bptree.ITree
145}
146
147// AddIndex adds a new index to the collection with the specified options
148//
149// Parameters:
150//   - name: the unique name of the index (e.g., "tags")
151//   - indexFn: a function that extracts either a string or []string from an object
152//   - options: bit flags for index configuration (e.g., UniqueIndex)
153func (c *Collection) AddIndex(name string, indexFn any, options IndexOption) {
154	if name == IDIndex {
155		panic("_id is a reserved index name")
156	}
157	c.indexes[name] = &Index{
158		fn:      indexFn,
159		options: options,
160		tree:    bptree.NewBPTree32(),
161	}
162}
163
164// storeIndex handles how we store an ID in the index tree
165func (idx *Index) store(key string, idStr string) {
166	stored := idx.tree.Get(key)
167	if stored == nil {
168		// First entry for this key
169		idx.tree.Set(key, idStr)
170		return
171	}
172
173	// Handle existing entries
174	switch existing := stored.(type) {
175	case string:
176		if existing == idStr {
177			return // Already stored
178		}
179		// Convert to array
180		idx.tree.Set(key, []string{existing, idStr})
181	case []string:
182		// Check if ID already exists
183		for _, id := range existing {
184			if id == idStr {
185				return
186			}
187		}
188		// Append new ID
189		idx.tree.Set(key, append(existing, idStr))
190	}
191}
192
193// removeIndex handles how we remove an ID from the index tree
194func (idx *Index) remove(key string, idStr string) {
195	stored := idx.tree.Get(key)
196	if stored == nil {
197		return
198	}
199
200	switch existing := stored.(type) {
201	case string:
202		if existing == idStr {
203			idx.tree.Remove(key)
204		}
205	case []string:
206		newIds := make([]string, 0, len(existing))
207		for _, id := range existing {
208			if id != idStr {
209				newIds = append(newIds, id)
210			}
211		}
212		if len(newIds) == 0 {
213			idx.tree.Remove(key)
214		} else if len(newIds) == 1 {
215			idx.tree.Set(key, newIds[0])
216		} else {
217			idx.tree.Set(key, newIds)
218		}
219	}
220}
221
222// generateKeys extracts one or more keys from an object for a given index.
223func generateKeys(idx *Index, obj any) ([]string, bool) {
224	if obj == nil {
225		return nil, false
226	}
227
228	switch fnTyped := idx.fn.(type) {
229	case func(any) string:
230		// Single-value index
231		key := fnTyped(obj)
232		return []string{key}, true
233	case func(any) []string:
234		// Multi-value index
235		keys := fnTyped(obj)
236		return keys, true
237	default:
238		panic("invalid index function type")
239	}
240}
241
242// Set adds or updates an object in the collection.
243// Returns a positive ID if successful.
244// Returns 0 if:
245//   - The object is nil
246//   - A uniqueness constraint would be violated
247//   - Index generation fails for any index
248func (c *Collection) Set(obj any) uint64 {
249	if obj == nil {
250		return 0
251	}
252
253	// Generate new ID
254	id := c.idGen.Next()
255	idStr := id.String()
256
257	// Check uniqueness constraints first
258	for name, idx := range c.indexes {
259		if name == IDIndex {
260			continue
261		}
262		keys, ok := generateKeys(idx, obj)
263		if !ok {
264			return 0
265		}
266
267		for _, key := range keys {
268			// Skip empty values for sparse indexes
269			if idx.options&SparseIndex != 0 && key == "" {
270				continue
271			}
272			if idx.options&CaseInsensitiveIndex != 0 {
273				key = strings.ToLower(key)
274			}
275			// Only check uniqueness for unique + single-value indexes
276			// (UniqueIndex is ambiguous; skipping that scenario)
277			if idx.options&UniqueIndex != 0 {
278				if existing := idx.tree.Get(key); existing != nil {
279					return 0
280				}
281			}
282		}
283	}
284
285	// Store in _id index first (the actual object)
286	c.indexes[IDIndex].tree.Set(idStr, obj)
287
288	// Store in all other indexes
289	for name, idx := range c.indexes {
290		if name == IDIndex {
291			continue
292		}
293		keys, ok := generateKeys(idx, obj)
294		if !ok {
295			// Rollback: remove from _id index
296			c.indexes[IDIndex].tree.Remove(idStr)
297			return 0
298		}
299
300		for _, key := range keys {
301			if idx.options&SparseIndex != 0 && key == "" {
302				continue
303			}
304			if idx.options&CaseInsensitiveIndex != 0 {
305				key = strings.ToLower(key)
306			}
307			idx.store(key, idStr)
308		}
309	}
310
311	return uint64(id)
312}
313
314// Get retrieves entries matching the given key in the specified index.
315// Returns an iterator over the matching entries.
316func (c *Collection) Get(indexName string, key string) EntryIterator {
317	idx, exists := c.indexes[indexName]
318	if !exists {
319		return EntryIterator{err: errors.New("index not found: " + indexName)}
320	}
321
322	if idx.options&CaseInsensitiveIndex != 0 {
323		key = strings.ToLower(key)
324	}
325
326	if indexName == IDIndex {
327		// For ID index, validate the ID format first
328		_, err := seqid.FromString(key)
329		if err != nil {
330			return EntryIterator{err: err}
331		}
332	}
333
334	return EntryIterator{
335		collection: c,
336		indexName:  indexName,
337		key:        key,
338	}
339}
340
341// GetFirst returns the first matching entry or nil if none found
342func (c *Collection) GetFirst(indexName, key string) *Entry {
343	iter := c.Get(indexName, key)
344	if iter.Next() {
345		return iter.Value()
346	}
347	return nil
348}
349
350// Delete removes an object by its ID and returns true if something was deleted
351func (c *Collection) Delete(id uint64) bool {
352	idStr := seqid.ID(id).String()
353
354	// Get the object first to clean up other indexes
355	obj := c.indexes[IDIndex].tree.Get(idStr)
356	if obj == nil {
357		return false
358	}
359
360	// Remove from all indexes
361	for name, idx := range c.indexes {
362		if name == IDIndex {
363			idx.tree.Remove(idStr)
364			continue
365		}
366		keys, ok := generateKeys(idx, obj)
367		if !ok {
368			continue
369		}
370		for _, key := range keys {
371			if idx.options&CaseInsensitiveIndex != 0 {
372				key = strings.ToLower(key)
373			}
374			idx.remove(key, idStr)
375		}
376	}
377	return true
378}
379
380// Update updates an existing object and returns true if successful
381// Returns true if the update was successful.
382// Returns false if:
383//   - The object is nil
384//   - The ID doesn't exist
385//   - A uniqueness constraint would be violated
386//   - Index generation fails for any index
387//
388// If the update fails, the collection remains unchanged.
389func (c *Collection) Update(id uint64, obj any) bool {
390	if obj == nil {
391		return false
392	}
393	idStr := seqid.ID(id).String()
394	oldObj := c.indexes[IDIndex].tree.Get(idStr)
395	if oldObj == nil {
396		return false
397	}
398
399	// Check unique constraints
400	for name, idx := range c.indexes {
401		if name == IDIndex {
402			continue
403		}
404
405		if idx.options&UniqueIndex != 0 {
406			newKeys, newOk := generateKeys(idx, obj)
407			_, oldOk := generateKeys(idx, oldObj)
408			if !newOk || !oldOk {
409				return false
410			}
411
412			for _, newKey := range newKeys {
413				if idx.options&CaseInsensitiveIndex != 0 {
414					newKey = strings.ToLower(newKey)
415				}
416
417				found := idx.tree.Get(newKey)
418				if found != nil {
419					if storedID, ok := found.(string); !ok || storedID != idStr {
420						return false
421					}
422				}
423			}
424		}
425	}
426
427	// Store old index entries for potential rollback
428	oldEntries := make(map[string][]string)
429	for name, idx := range c.indexes {
430		if name == IDIndex {
431			continue
432		}
433		oldKeys, ok := generateKeys(idx, oldObj)
434		if !ok {
435			continue
436		}
437		var adjusted []string
438		for _, okey := range oldKeys {
439			if idx.options&CaseInsensitiveIndex != 0 {
440				okey = strings.ToLower(okey)
441			}
442			// Remove the oldObj from the index right away
443			idx.remove(okey, idStr)
444			adjusted = append(adjusted, okey)
445		}
446		oldEntries[name] = adjusted
447	}
448
449	// Update the object in the _id index
450	c.indexes[IDIndex].tree.Set(idStr, obj)
451
452	// Add new index entries
453	for name, idx := range c.indexes {
454		if name == IDIndex {
455			continue
456		}
457		newKeys, ok := generateKeys(idx, obj)
458		if !ok {
459			// Rollback: restore old object and old index entries
460			c.indexes[IDIndex].tree.Set(idStr, oldObj)
461			for idxName, keys := range oldEntries {
462				for _, oldKey := range keys {
463					c.indexes[idxName].store(oldKey, idStr)
464				}
465			}
466			return false
467		}
468		for _, nkey := range newKeys {
469			if idx.options&CaseInsensitiveIndex != 0 {
470				nkey = strings.ToLower(nkey)
471			}
472			idx.store(nkey, idStr)
473		}
474	}
475
476	return true
477}
478
479// GetAll retrieves all entries matching the given key in the specified index.
480func (c *Collection) GetAll(indexName string, key string) []Entry {
481	idx, exists := c.indexes[indexName]
482	if !exists {
483		return nil
484	}
485
486	if idx.options&CaseInsensitiveIndex != 0 {
487		key = strings.ToLower(key)
488	}
489
490	if indexName == IDIndex {
491		if obj := idx.tree.Get(key); obj != nil {
492			return []Entry{{ID: key, Obj: obj}}
493		}
494		return nil
495	}
496
497	idData := idx.tree.Get(key)
498	if idData == nil {
499		return nil
500	}
501
502	// Handle both single and multi-value cases based on the actual data type
503	switch stored := idData.(type) {
504	case []string:
505		result := make([]Entry, 0, len(stored))
506		for _, idStr := range stored {
507			if obj := c.indexes[IDIndex].tree.Get(idStr); obj != nil {
508				result = append(result, Entry{ID: idStr, Obj: obj})
509			}
510		}
511		return result
512	case string:
513		if obj := c.indexes[IDIndex].tree.Get(stored); obj != nil {
514			return []Entry{{ID: stored, Obj: obj}}
515		}
516	}
517	return nil
518}
519
520// GetIndex returns the underlying tree for an index
521func (c *Collection) GetIndex(name string) bptree.ITree {
522	idx, exists := c.indexes[name]
523	if !exists {
524		return nil
525	}
526	return idx.tree
527}