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