Skip to content

Commit eaa4757

Browse files
committed
Added search engine
1 parent d6d05cb commit eaa4757

File tree

8 files changed

+978
-0
lines changed

8 files changed

+978
-0
lines changed

db/db.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,15 @@ type Recommendation struct {
339339
RecommendedVal int64
340340
}
341341

342+
// BadInputError Special error indicating bad user input as opposed to a database error
343+
type BadInputError struct {
344+
Details error
345+
}
346+
347+
func (bi BadInputError) Error() string {
348+
return bi.Details.Error()
349+
}
350+
342351
// DBType - database type
343352
type DBType struct {
344353
Driver DialectName // driver name (used in the code)

db/helpers.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,10 @@ func ParseScheme(s string) (scheme string, uri string, err error) {
277277
return parts[0], parts[1], nil
278278
}
279279

280+
func FormatTimeStamp(timestamp time.Time) string {
281+
return fmt.Sprintf("%vns", timestamp.UTC().UnixNano())
282+
}
283+
280284
// Cond represents a condition
281285
type Cond struct {
282286
Col string

db/search/compare.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package search
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/acronis/perfkit/db"
7+
)
8+
9+
type comparator[T Searchable[T]] func(a, b T) bool
10+
type comparators[T Searchable[T]] map[string]map[string]comparator[T]
11+
12+
func makeComparator[T Searchable[T]](values []string, comparable comparators[T]) (comparator[T], error) {
13+
var less func(a, b T) bool
14+
if len(values) == 0 {
15+
return less, nil
16+
}
17+
18+
var finalLess func(a, b T) bool
19+
20+
for i := len(values) - 1; i >= 0; i-- {
21+
value := values[i]
22+
23+
fnc, field, err := db.ParseFunc(value)
24+
if err != nil {
25+
return nil, err
26+
}
27+
28+
if fnc == "" {
29+
return nil, fmt.Errorf("empty order function")
30+
}
31+
32+
if field == "" {
33+
return nil, fmt.Errorf("empty order field")
34+
}
35+
36+
fieldComparators, ok := comparable[field]
37+
if !ok {
38+
return nil, fmt.Errorf("bad order field '%v'", field)
39+
}
40+
41+
less, ok := fieldComparators[fnc]
42+
if !ok {
43+
return nil, fmt.Errorf("bad order function '%v'", fnc)
44+
}
45+
46+
if finalLess == nil {
47+
finalLess = less
48+
} else {
49+
var deepLess = finalLess
50+
51+
finalLess = func(a, b T) bool {
52+
if less(a, b) {
53+
return true
54+
} else if less(b, a) {
55+
return false
56+
}
57+
58+
return deepLess(a, b)
59+
}
60+
}
61+
}
62+
63+
return finalLess, nil
64+
}

db/search/cursor.go

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
package search
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/acronis/perfkit/db"
7+
)
8+
9+
type sorting struct {
10+
Field string
11+
Func string
12+
}
13+
14+
func uniqueSort(encodedSorts []string, uniqueFields map[string]bool, cursors map[string]string) ([]string, []sorting, error) {
15+
var hasUniqueSorting = false
16+
var uniqueOrderDirection int
17+
18+
var encoded []string
19+
var sorts []sorting
20+
21+
for _, v := range encodedSorts {
22+
var fnc, field, err = db.ParseFunc(v)
23+
if err != nil {
24+
return nil, nil, err
25+
}
26+
27+
var nullable, unique = uniqueFields[field]
28+
hasUniqueSorting = hasUniqueSorting || (unique && !nullable)
29+
30+
encoded = append(encoded, v)
31+
sorts = append(sorts, sorting{
32+
Field: field,
33+
Func: fnc,
34+
})
35+
36+
switch fnc {
37+
case "asc":
38+
uniqueOrderDirection++
39+
case "desc":
40+
uniqueOrderDirection--
41+
}
42+
43+
if unique {
44+
if !nullable {
45+
break
46+
} else if cursors != nil {
47+
if val, ok := cursors[field]; ok && val != db.SpecialConditionIsNull {
48+
if fnc != "desc" {
49+
break
50+
}
51+
}
52+
}
53+
}
54+
}
55+
56+
if !hasUniqueSorting {
57+
if uniqueOrderDirection >= 0 {
58+
encoded = append(encoded, "asc(id)")
59+
sorts = append(sorts, sorting{Field: "id", Func: "asc"})
60+
} else {
61+
encoded = append(encoded, "desc(id)")
62+
sorts = append(sorts, sorting{Field: "id", Func: "desc"})
63+
}
64+
}
65+
66+
return encoded, sorts, nil
67+
}
68+
69+
func orderCondition(val, fnc string) (expr string, flag bool, err error) {
70+
var direction string
71+
switch fnc {
72+
case "asc":
73+
switch val {
74+
case db.SpecialConditionIsNull:
75+
return db.SpecialConditionIsNotNull, false, nil
76+
case db.SpecialConditionIsNotNull:
77+
return "", true, nil
78+
default:
79+
direction = "gt"
80+
}
81+
case "desc":
82+
switch val {
83+
case db.SpecialConditionIsNotNull:
84+
return db.SpecialConditionIsNull, false, nil
85+
case db.SpecialConditionIsNull:
86+
return "", true, nil
87+
default:
88+
direction = "lt"
89+
}
90+
default:
91+
return "", false, fmt.Errorf("missing ordering for cursor")
92+
}
93+
94+
return fmt.Sprintf("%s(%v)", direction, val), false, nil
95+
}
96+
97+
func splitQueryOnLightWeightQueries(pt PageToken, uniqueFields map[string]bool) ([]PageToken, error) {
98+
var tokens []PageToken
99+
100+
if len(pt.Fields) == 0 {
101+
tokens = append(tokens, pt)
102+
return tokens, nil
103+
}
104+
105+
// check for unique sorting
106+
var encodedSorts, sorts, err = uniqueSort(pt.Order, uniqueFields, pt.Cursor)
107+
if err != nil {
108+
return nil, err
109+
}
110+
111+
if len(pt.Cursor) == 0 {
112+
pt.Order = encodedSorts
113+
tokens = append(tokens, pt)
114+
return tokens, nil
115+
}
116+
117+
// construct sort map for fast access
118+
var orderFunctions = map[string]string{}
119+
for _, sort := range sorts {
120+
orderFunctions[sort.Field] = sort.Func
121+
}
122+
123+
// add condition based on cursor
124+
var whereFromCursor = func(fld, val string, pt *PageToken) (bool, error) {
125+
var filter, empty, filterErr = orderCondition(val, orderFunctions[fld])
126+
if filterErr != nil {
127+
return false, filterErr
128+
}
129+
130+
if empty {
131+
return true, nil
132+
}
133+
134+
pt.Filter[fld] = append(pt.Filter[fld], filter)
135+
return false, nil
136+
}
137+
138+
for cursor := range pt.Cursor {
139+
if _, ok := orderFunctions[cursor]; !ok {
140+
return nil, fmt.Errorf("prohibited cursor, not mentioned it order: %v", cursor)
141+
}
142+
}
143+
144+
// split to x page tokens
145+
for i := range sorts {
146+
var cpt = pt
147+
var last = len(sorts) - 1 - i
148+
149+
// copy filters
150+
cpt.Filter = make(map[string][]string, len(sorts)-1-i)
151+
for k, v := range pt.Filter {
152+
cpt.Filter[k] = v
153+
}
154+
155+
// add equal condition on all fields except last in sorts
156+
for j := 0; j <= last-1; j++ {
157+
var fld = sorts[j].Field
158+
var val = pt.Cursor[fld]
159+
160+
cpt.Filter[fld] = append(cpt.Filter[fld], val)
161+
}
162+
163+
// add gt / lt condition for last sorting
164+
var empty bool
165+
if val, ok := cpt.Cursor[sorts[last].Field]; ok {
166+
if empty, err = whereFromCursor(sorts[last].Field, val, &cpt); err != nil {
167+
return nil, err
168+
}
169+
} else {
170+
continue
171+
}
172+
173+
if empty {
174+
continue
175+
}
176+
177+
// Add only needed sort to cpt
178+
cpt.Order = []string{}
179+
for j := last; j <= len(sorts)-1; j++ {
180+
cpt.Order = append(cpt.Order, encodedSorts[j])
181+
182+
var sortField = sorts[j].Field
183+
if nullable, unique := uniqueFields[sortField]; unique {
184+
if !nullable {
185+
break
186+
}
187+
188+
var becomeUnique = false
189+
// for ASC if we have a value, that means we already select all null rows
190+
// for DESC Nulls can start at any row
191+
if sorts[j].Func == "asc" {
192+
for _, val := range cpt.Filter[sortField] {
193+
if val != db.SpecialConditionIsNull {
194+
becomeUnique = true
195+
break
196+
}
197+
}
198+
}
199+
if becomeUnique {
200+
break
201+
}
202+
}
203+
}
204+
205+
cpt.Cursor = nil
206+
207+
tokens = append(tokens, cpt)
208+
}
209+
210+
return tokens, nil
211+
}
212+
213+
func createNextCursorBasedPageToken[T Searchable[T]](previousPageToken PageToken, items []T, limit int64,
214+
cursorGen func(entity *T, field string) (string, error), uniqueFields map[string]bool) (*PageToken, error) {
215+
if int64(len(items)) < limit {
216+
return nil, nil
217+
}
218+
219+
var pt PageToken
220+
pt.Cursor = make(map[string]string)
221+
pt.Fields = previousPageToken.Fields
222+
223+
var encoded, sorts, err = uniqueSort(previousPageToken.Order, uniqueFields, previousPageToken.Cursor)
224+
if err != nil {
225+
return nil, err
226+
}
227+
pt.Order = encoded
228+
229+
var last = items[len(items)-1]
230+
for _, sort := range sorts {
231+
var value string
232+
if value, err = cursorGen(&last, sort.Field); err != nil {
233+
return nil, err
234+
}
235+
pt.Cursor[sort.Field] = value
236+
}
237+
238+
return &pt, nil
239+
}

0 commit comments

Comments
 (0)