storage.gno

5.84 Kb ยท 234 lines
  1package datastore
  2
  3import (
  4	"errors"
  5	"strings"
  6
  7	"gno.land/p/demo/seqid"
  8	"gno.land/p/moul/collection"
  9)
 10
 11// NewStorage creates a new records storage.
 12func NewStorage(name string, options ...StorageOption) Storage {
 13	s := Storage{
 14		name:       name,
 15		collection: collection.New(),
 16		schema:     NewSchema(strings.Title(name)),
 17	}
 18
 19	for _, apply := range options {
 20		apply(&s)
 21	}
 22	return s
 23}
 24
 25// Storage stores a collection of records.
 26//
 27// By default it searches records by record ID but it allows
 28// using custom user defined indexes for other record fields.
 29//
 30// When a storage is created it defines a default schema that
 31// keeps track of record fields. Storage can be optionally
 32// created with a user defined schema in cases where the number
 33// of fields has to be pre-defined or when new records must have
 34// one or more fields initialized to default values.
 35type Storage struct {
 36	name       string
 37	collection *collection.Collection
 38	schema     *Schema
 39}
 40
 41// Name returns storage's name.
 42func (s Storage) Name() string {
 43	return s.name
 44}
 45
 46// Collection returns the undelying collection used by the
 47// storage to store all records.
 48func (s Storage) Collection() *collection.Collection {
 49	return s.collection
 50}
 51
 52// Schema returns the schema being used to track record fields.
 53func (s Storage) Schema() *Schema {
 54	return s.schema
 55}
 56
 57// Size returns the number of records that the storage have.
 58func (s Storage) Size() int {
 59	return s.collection.GetIndex(collection.IDIndex).Size()
 60}
 61
 62// NewRecord creates a new storage record.
 63//
 64// If a custom schema with default field values is assigned to
 65// storage it's used to assign initial default values when new
 66// records are created.
 67//
 68// Creating a new record doesn't assign an ID to it, a new ID
 69// is generated and assigned to the record when it's saved for
 70// the first time.
 71func (s Storage) NewRecord() Record {
 72	r := &record{
 73		schema:     s.schema,
 74		collection: s.collection,
 75	}
 76
 77	// Assign default record values if the schema defines them
 78	for i, name := range s.schema.Fields() {
 79		if v, found := s.schema.GetDefaultByIndex(i); found {
 80			r.Set(name, v)
 81		}
 82	}
 83	return r
 84}
 85
 86// Query returns a recordset that matches the query parameters.
 87// By default query selects records using the ID index.
 88//
 89// Example usage:
 90//
 91//	// Get 50 records starting from the one at position 100
 92//	rs, _ := storage.Query(
 93//		WithOffset(100),
 94//		WithSize(50),
 95//	)
 96//
 97//	// Iterate records to create a new slice
 98//	var records []Record
 99//	rs.Iterate(func (r Record) bool {
100//		records = append(records, r)
101//		return false
102//	})
103func (s Storage) Query(options ...QueryOption) (Recordset, error) {
104	// Initialize the recordset for the query
105	rs := recordset{
106		query:   defaultQuery,
107		records: s.collection.GetIndex(collection.IDIndex),
108	}
109
110	for _, apply := range options {
111		if err := apply(&rs.query); err != nil {
112			return nil, err
113		}
114	}
115
116	indexName := rs.query.IndexName()
117	if indexName != collection.IDIndex {
118		// When using a custom index get the keys to get records from the ID index
119		keys, err := s.getIndexRecordsKeys(indexName, rs.query.IndexKey())
120		if err != nil {
121			return nil, err
122		}
123
124		// Adjust the number of keys to match available query options
125		if offset := rs.query.Offset(); offset > 0 {
126			if offset > len(keys) {
127				keys = nil
128			} else {
129				keys = keys[offset:]
130			}
131		}
132
133		if size := rs.query.Size(); size > 0 && size < len(keys) {
134			keys = keys[:size]
135		}
136
137		rs.keys = keys
138		rs.size = len(keys)
139	} else {
140		// When using the default ID index init size with the total number of records
141		rs.size = rs.records.Size()
142
143		// Adjust recordset size to match available query options
144		if offset := rs.query.Offset(); offset > 0 {
145			if offset > rs.size {
146				rs.size = 0
147			} else {
148				rs.size -= offset
149			}
150		}
151
152		if size := rs.query.Size(); size > 0 && size < rs.size {
153			rs.size = size
154		}
155	}
156
157	return rs, nil
158}
159
160// MustQuery returns a recordset that matches the query parameters or panics on error.
161// By default query selects records using the ID index.
162//
163// Example usage:
164//
165//	// Get 50 records starting from the one at position 100
166//	var records []Record
167//	storage.MustQuery(
168//		WithOffset(100),
169//		WithSize(50),
170//	).Iterate(func (r Record) bool {
171//		records = append(records, r)
172//		return false
173//	})
174func (s Storage) MustQuery(options ...QueryOption) Recordset {
175	rs, err := s.Query(options...)
176	if err != nil {
177		panic(err)
178	}
179	return rs
180}
181
182// Get returns the first record found for a key within a storage index.
183//
184// This is a convenience method to get a single record. A multi index will
185// always return the first record value for the specified key in this case.
186// To get multiple records create a query using a custom index and key value
187// or use the underlying storage collection.
188func (s Storage) Get(indexName, indexKey string) (_ Record, found bool) {
189	iter := s.collection.Get(indexName, indexKey)
190	if iter.Next() {
191		return iter.Value().Obj.(Record), true
192	}
193	return nil, false
194}
195
196// GetByID returns a record whose ID matches the specified ID.
197func (s Storage) GetByID(id uint64) (_ Record, found bool) {
198	iter := s.collection.Get(collection.IDIndex, seqid.ID(id).String())
199	if iter.Next() {
200		return iter.Value().Obj.(Record), true
201	}
202	return nil, false
203}
204
205// Delete deletes a record from the storage.
206func (s Storage) Delete(id uint64) bool {
207	return s.collection.Delete(id)
208}
209
210func (s Storage) getIndexRecordsKeys(indexName, indexKey string) ([]string, error) {
211	idx := s.collection.GetIndex(indexName)
212	if idx == nil {
213		return nil, errors.New("storage index for query not found: " + indexName)
214	}
215
216	var keys []string
217	if v, found := idx.Get(indexKey); found {
218		keys = castIfaceToRecordKeys(v)
219		if keys == nil {
220			return nil, errors.New("unexpected storage index key format")
221		}
222	}
223	return keys, nil
224}
225
226func castIfaceToRecordKeys(v interface{}) []string {
227	switch k := v.(type) {
228	case []string:
229		return k
230	case string:
231		return []string{k}
232	}
233	return nil
234}