collection.gno
12.29 Kb ยท 509 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
63package collection
64
65import (
66 "errors"
67 "strings"
68
69 "gno.land/p/demo/avl"
70 "gno.land/p/demo/seqid"
71)
72
73// New creates a new Collection instance with an initialized ID index.
74// The ID index is a special unique index that is always present and
75// serves as the primary key for all objects in the collection.
76func New() *Collection {
77 c := &Collection{
78 indexes: make(map[string]*Index),
79 idGen: seqid.ID(0),
80 }
81 // Initialize _id index
82 c.indexes[IDIndex] = &Index{
83 options: UniqueIndex,
84 tree: avl.NewTree(),
85 }
86 return c
87}
88
89// Collection represents a collection of objects with multiple indexes
90type Collection struct {
91 indexes map[string]*Index
92 idGen seqid.ID
93}
94
95const (
96 // IDIndex is the reserved name for the primary key index
97 IDIndex = "_id"
98)
99
100// IndexOption represents configuration options for an index using bit flags
101type IndexOption uint64
102
103const (
104 // DefaultIndex is a basic index with no special options
105 DefaultIndex IndexOption = 0
106
107 // UniqueIndex ensures no duplicate values are allowed
108 UniqueIndex IndexOption = 1 << iota
109
110 // CaseInsensitiveIndex automatically converts string values to lowercase
111 CaseInsensitiveIndex
112
113 // SparseIndex only indexes non-empty values
114 SparseIndex
115)
116
117// Index represents an index with its configuration and data.
118// The index function can return either:
119// - string: for single-value indexes
120// - []string: for multi-value indexes where one object can be indexed under multiple keys
121//
122// The backing tree stores either a single ID or []string for multiple IDs per key.
123type Index struct {
124 fn any
125 options IndexOption
126 tree avl.ITree
127}
128
129// AddIndex adds a new index to the collection with the specified options
130//
131// Parameters:
132// - name: the unique name of the index (e.g., "tags")
133// - indexFn: a function that extracts either a string or []string from an object
134// - options: bit flags for index configuration (e.g., UniqueIndex)
135func (c *Collection) AddIndex(name string, indexFn any, options IndexOption) {
136 if name == IDIndex {
137 panic("_id is a reserved index name")
138 }
139 c.indexes[name] = &Index{
140 fn: indexFn,
141 options: options,
142 tree: avl.NewTree(),
143 }
144}
145
146// storeIndex handles how we store an ID in the index tree
147func (idx *Index) store(key string, idStr string) {
148 stored, exists := idx.tree.Get(key)
149 if !exists {
150 // First entry for this key
151 idx.tree.Set(key, idStr)
152 return
153 }
154
155 // Handle existing entries
156 switch existing := stored.(type) {
157 case string:
158 if existing == idStr {
159 return // Already stored
160 }
161 // Convert to array
162 idx.tree.Set(key, []string{existing, idStr})
163 case []string:
164 // Check if ID already exists
165 for _, id := range existing {
166 if id == idStr {
167 return
168 }
169 }
170 // Append new ID
171 idx.tree.Set(key, append(existing, idStr))
172 }
173}
174
175// removeIndex handles how we remove an ID from the index tree
176func (idx *Index) remove(key string, idStr string) {
177 stored, exists := idx.tree.Get(key)
178 if !exists {
179 return
180 }
181
182 switch existing := stored.(type) {
183 case string:
184 if existing == idStr {
185 idx.tree.Remove(key)
186 }
187 case []string:
188 newIds := make([]string, 0, len(existing))
189 for _, id := range existing {
190 if id != idStr {
191 newIds = append(newIds, id)
192 }
193 }
194 if len(newIds) == 0 {
195 idx.tree.Remove(key)
196 } else if len(newIds) == 1 {
197 idx.tree.Set(key, newIds[0])
198 } else {
199 idx.tree.Set(key, newIds)
200 }
201 }
202}
203
204// generateKeys extracts one or more keys from an object for a given index.
205func generateKeys(idx *Index, obj any) ([]string, bool) {
206 if obj == nil {
207 return nil, false
208 }
209
210 switch fnTyped := idx.fn.(type) {
211 case func(any) string:
212 // Single-value index
213 key := fnTyped(obj)
214 return []string{key}, true
215 case func(any) []string:
216 // Multi-value index
217 keys := fnTyped(obj)
218 return keys, true
219 default:
220 panic("invalid index function type")
221 }
222}
223
224// Set adds or updates an object in the collection.
225// Returns a positive ID if successful.
226// Returns 0 if:
227// - The object is nil
228// - A uniqueness constraint would be violated
229// - Index generation fails for any index
230func (c *Collection) Set(obj any) uint64 {
231 if obj == nil {
232 return 0
233 }
234
235 // Generate new ID
236 id := c.idGen.Next()
237 idStr := id.String()
238
239 // Check uniqueness constraints first
240 for name, idx := range c.indexes {
241 if name == IDIndex {
242 continue
243 }
244 keys, ok := generateKeys(idx, obj)
245 if !ok {
246 return 0
247 }
248
249 for _, key := range keys {
250 // Skip empty values for sparse indexes
251 if idx.options&SparseIndex != 0 && key == "" {
252 continue
253 }
254 if idx.options&CaseInsensitiveIndex != 0 {
255 key = strings.ToLower(key)
256 }
257 // Only check uniqueness for unique + single-value indexes
258 // (UniqueIndex is ambiguous; skipping that scenario)
259 if idx.options&UniqueIndex != 0 {
260 if existing, exists := idx.tree.Get(key); exists && existing != nil {
261 return 0
262 }
263 }
264 }
265 }
266
267 // Store in _id index first (the actual object)
268 c.indexes[IDIndex].tree.Set(idStr, obj)
269
270 // Store in all other indexes
271 for name, idx := range c.indexes {
272 if name == IDIndex {
273 continue
274 }
275 keys, ok := generateKeys(idx, obj)
276 if !ok {
277 // Rollback: remove from _id index
278 c.indexes[IDIndex].tree.Remove(idStr)
279 return 0
280 }
281
282 for _, key := range keys {
283 if idx.options&SparseIndex != 0 && key == "" {
284 continue
285 }
286 if idx.options&CaseInsensitiveIndex != 0 {
287 key = strings.ToLower(key)
288 }
289 idx.store(key, idStr)
290 }
291 }
292
293 return uint64(id)
294}
295
296// Get retrieves entries matching the given key in the specified index.
297// Returns an iterator over the matching entries.
298func (c *Collection) Get(indexName string, key string) EntryIterator {
299 idx, exists := c.indexes[indexName]
300 if !exists {
301 return EntryIterator{err: errors.New("index not found: " + indexName)}
302 }
303
304 if idx.options&CaseInsensitiveIndex != 0 {
305 key = strings.ToLower(key)
306 }
307
308 if indexName == IDIndex {
309 // For ID index, validate the ID format first
310 _, err := seqid.FromString(key)
311 if err != nil {
312 return EntryIterator{err: err}
313 }
314 }
315
316 return EntryIterator{
317 collection: c,
318 indexName: indexName,
319 key: key,
320 }
321}
322
323// GetFirst returns the first matching entry or nil if none found
324func (c *Collection) GetFirst(indexName, key string) *Entry {
325 iter := c.Get(indexName, key)
326 if iter.Next() {
327 return iter.Value()
328 }
329 return nil
330}
331
332// Delete removes an object by its ID and returns true if something was deleted
333func (c *Collection) Delete(id uint64) bool {
334 idStr := seqid.ID(id).String()
335
336 // Get the object first to clean up other indexes
337 obj, exists := c.indexes[IDIndex].tree.Get(idStr)
338 if !exists {
339 return false
340 }
341
342 // Remove from all indexes
343 for name, idx := range c.indexes {
344 if name == IDIndex {
345 idx.tree.Remove(idStr)
346 continue
347 }
348 keys, ok := generateKeys(idx, obj)
349 if !ok {
350 continue
351 }
352 for _, key := range keys {
353 if idx.options&CaseInsensitiveIndex != 0 {
354 key = strings.ToLower(key)
355 }
356 idx.remove(key, idStr)
357 }
358 }
359 return true
360}
361
362// Update updates an existing object and returns true if successful
363// Returns true if the update was successful.
364// Returns false if:
365// - The object is nil
366// - The ID doesn't exist
367// - A uniqueness constraint would be violated
368// - Index generation fails for any index
369//
370// If the update fails, the collection remains unchanged.
371func (c *Collection) Update(id uint64, obj any) bool {
372 if obj == nil {
373 return false
374 }
375 idStr := seqid.ID(id).String()
376 oldObj, exists := c.indexes[IDIndex].tree.Get(idStr)
377 if !exists {
378 return false
379 }
380
381 // Check unique constraints
382 for name, idx := range c.indexes {
383 if name == IDIndex {
384 continue
385 }
386
387 if idx.options&UniqueIndex != 0 {
388 newKeys, newOk := generateKeys(idx, obj)
389 _, oldOk := generateKeys(idx, oldObj)
390 if !newOk || !oldOk {
391 return false
392 }
393
394 for _, newKey := range newKeys {
395 if idx.options&CaseInsensitiveIndex != 0 {
396 newKey = strings.ToLower(newKey)
397 }
398
399 found, _ := idx.tree.Get(newKey)
400 if found != nil {
401 if storedID, ok := found.(string); !ok || storedID != idStr {
402 return false
403 }
404 }
405 }
406 }
407 }
408
409 // Store old index entries for potential rollback
410 oldEntries := make(map[string][]string)
411 for name, idx := range c.indexes {
412 if name == IDIndex {
413 continue
414 }
415 oldKeys, ok := generateKeys(idx, oldObj)
416 if !ok {
417 continue
418 }
419 var adjusted []string
420 for _, okey := range oldKeys {
421 if idx.options&CaseInsensitiveIndex != 0 {
422 okey = strings.ToLower(okey)
423 }
424 // Remove the oldObj from the index right away
425 idx.remove(okey, idStr)
426 adjusted = append(adjusted, okey)
427 }
428 oldEntries[name] = adjusted
429 }
430
431 // Update the object in the _id index
432 c.indexes[IDIndex].tree.Set(idStr, obj)
433
434 // Add new index entries
435 for name, idx := range c.indexes {
436 if name == IDIndex {
437 continue
438 }
439 newKeys, ok := generateKeys(idx, obj)
440 if !ok {
441 // Rollback: restore old object and old index entries
442 c.indexes[IDIndex].tree.Set(idStr, oldObj)
443 for idxName, keys := range oldEntries {
444 for _, oldKey := range keys {
445 c.indexes[idxName].store(oldKey, idStr)
446 }
447 }
448 return false
449 }
450 for _, nkey := range newKeys {
451 if idx.options&CaseInsensitiveIndex != 0 {
452 nkey = strings.ToLower(nkey)
453 }
454 idx.store(nkey, idStr)
455 }
456 }
457
458 return true
459}
460
461// GetAll retrieves all entries matching the given key in the specified index.
462func (c *Collection) GetAll(indexName string, key string) []Entry {
463 idx, exists := c.indexes[indexName]
464 if !exists {
465 return nil
466 }
467
468 if idx.options&CaseInsensitiveIndex != 0 {
469 key = strings.ToLower(key)
470 }
471
472 if indexName == IDIndex {
473 if obj, exists := idx.tree.Get(key); exists {
474 return []Entry{{ID: key, Obj: obj}}
475 }
476 return nil
477 }
478
479 idData, exists := idx.tree.Get(key)
480 if !exists {
481 return nil
482 }
483
484 // Handle both single and multi-value cases based on the actual data type
485 switch stored := idData.(type) {
486 case []string:
487 result := make([]Entry, 0, len(stored))
488 for _, idStr := range stored {
489 if obj, exists := c.indexes[IDIndex].tree.Get(idStr); exists {
490 result = append(result, Entry{ID: idStr, Obj: obj})
491 }
492 }
493 return result
494 case string:
495 if obj, exists := c.indexes[IDIndex].tree.Get(stored); exists {
496 return []Entry{{ID: stored, Obj: obj}}
497 }
498 }
499 return nil
500}
501
502// GetIndex returns the underlying tree for an index
503func (c *Collection) GetIndex(name string) avl.ITree {
504 idx, exists := c.indexes[name]
505 if !exists {
506 return nil
507 }
508 return idx.tree
509}