-
Notifications
You must be signed in to change notification settings - Fork 5
/
filter.go
252 lines (198 loc) · 5.96 KB
/
filter.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
package tormenta
import (
"reflect"
"time"
"github.com/dgraph-io/badger"
"github.com/jpincas/gouuidv6"
)
type filter struct {
////////////////////////////
// Copied from main query //
////////////////////////////
// From and To dates
from, to gouuidv6.UUID
// Reverse?
reverse bool
// Name of the entity -> key root
keyRoot []byte
// Limit number of returned results
limit int
// Offet - start returning results N entities from the beginning
// offsetCounter used to track the offset
offset, offsetCounter int
/////////////////////////////
// Specific to this filter //
/////////////////////////////
// The start and end points of the index range search
start, end interface{}
// Name of the index on which to apply filter
indexName []byte
indexKind reflect.Kind
// Is this a 'starts with' index query
isStartsWithQuery bool
// Ranges and comparision key
seekFrom, validTo, compareTo []byte
// Is already prepared?
prepared bool
}
func (f filter) isIndexRangeSearch() bool {
return f.start != f.end && !f.isStartsWithQuery
}
func (f filter) isExactIndexMatchSearch() bool {
return f.start == f.end && f.start != nil && f.end != nil
}
func (f *filter) prepare() error {
// 'starts with' type query doesn't work with reverse
// so switch it back to a regular search
if f.isStartsWithQuery && f.reverse {
f.reverse = false
}
f.setFromToIfEmpty()
err := f.setRanges()
if err != nil {
return err
}
// Mark as prepared
f.prepared = true
return nil
}
func (f *filter) setFromToIfEmpty() {
// For index range searches - we don't do this, so exit right away
if f.isIndexRangeSearch() {
return
}
// If 'from' or 'to' have not been specified manually by the user,
// then we set them to the 'widest' times possible,
// i.e. 'between beginning of time' and 'now'
// If we don't do this, then some searches work OK, but particuarly reversed searches
// can experience strange behaviour (namely returned 0 results), because the iteration
// ends up starting from the end of the list.
// Another side-effect of not doing this is that exact match string searches would become 'starts with' searches. We might want that behaviour though, so we include a check for this type of search below
t1 := time.Time{}
t2 := time.Now()
if f.from.IsNil() {
// If we are doing a 'starts with' query,
// then we DON'T want to set the from point
// This magically gives us 'starts with'
// instead of exact match,
// BUT - this trick only works for forward searches,
// not 'reverse' searches,
// so there is a protection in the query preparation
if !f.isStartsWithQuery {
f.from = fromUUID(t1)
}
}
if f.to.IsNil() {
f.to = toUUID(t2)
}
}
func (f *filter) setRanges() error {
var seekFrom, validTo, compareTo []byte
// For reverse queries, flick-flack start/end and from/to
// to provide a standardised user API
if f.reverse {
tempEnd := f.end
f.end = f.start
f.start = tempEnd
tempTo := f.to
f.to = f.from
f.from = tempTo
}
startBytes, err := interfaceToBytesWithOverride(f.start, f.indexKind)
if err != nil {
return err
}
endBytes, err := interfaceToBytesWithOverride(f.end, f.indexKind)
if err != nil {
return err
}
if f.isExactIndexMatchSearch() {
// For index searches with exact match
seekFrom = newIndexMatchKey(f.keyRoot, f.indexName, startBytes, f.from).bytes()
validTo = newIndexMatchKey(f.keyRoot, f.indexName, endBytes).bytes()
compareTo = newIndexMatchKey(f.keyRoot, f.indexName, endBytes, f.to).bytes()
} else {
// For regular index searches
seekFrom = newIndexKey(f.keyRoot, f.indexName, startBytes).bytes()
validTo = newIndexKey(f.keyRoot, f.indexName, nil).bytes()
compareTo = newIndexKey(f.keyRoot, f.indexName, endBytes).bytes()
}
// For reverse queries, append the byte 0xFF to get inclusive results
// See Badger issue: https://github.com/dgraph-io/badger/issues/347
if f.reverse {
seekFrom = append(seekFrom, 0xFF)
}
f.seekFrom = seekFrom
f.validTo = validTo
f.compareTo = compareTo
return nil
}
func (f filter) endIteration(it *badger.Iterator, noIDsSoFar int) bool {
if it.ValidForPrefix(f.validTo) {
if f.isLimitMet(noIDsSoFar) || f.isEndOfRange(it) {
return false
}
return true
}
return false
}
func (f filter) shouldStripKeyID() bool {
// Index queries which are exact match AND have a 'to' clause
// also never need to have ID stripped
if f.isExactIndexMatchSearch() && !f.to.IsNil() {
return false
}
return true
}
func (f filter) isEndOfRange(it *badger.Iterator) bool {
key := it.Item().Key()
return f.end != nil && compareKeyBytes(f.compareTo, key, f.reverse, f.shouldStripKeyID())
}
func (f filter) isLimitMet(noIDsSoFar int) bool {
return f.limit > 0 && noIDsSoFar >= f.limit
}
func (f *filter) reset() {
f.offsetCounter = f.offset
}
func (f filter) getIteratorOptions() badger.IteratorOptions {
options := badger.DefaultIteratorOptions
options.Reverse = f.reverse
options.PrefetchValues = false
return options
}
func (f *filter) queryIDs(txn *badger.Txn) (ids idList, err error) {
if !f.prepared {
err = f.prepare()
if err != nil {
return ids, err
}
}
f.reset()
it := txn.NewIterator(f.getIteratorOptions())
defer it.Close()
for it.Seek(f.seekFrom); f.endIteration(it, ids.length()); it.Next() {
// If this is a 'range index' type Query
// that ALSO has a date range, the procedure is a little more complicated
// compared to an exact index match.
// Since the start/end points of the iteration focus on the index, e.g. E-J (alphabetical index)
// we need to manually check all the keys and reject those that don't fit the date range
if !f.isExactIndexMatchSearch() {
key := extractID(it.Item().Key())
if keyIsOutsideDateRange(key, f.from, f.to) {
continue
}
}
// Skip the first N entities according to the specified offset
if f.offsetCounter > 0 {
f.offsetCounter--
continue
}
item := it.Item()
ids = append(ids, extractID(item.Key()))
}
return
}
// Helpers
func toIndexName(s string) []byte {
return []byte(s)
}