Skip to content

Commit 8a0813e

Browse files
authored
Merge pull request #120 from qubic/feature/improve-query
Feature/improve query
2 parents 3ccc9a0 + 6f7e9be commit 8a0813e

File tree

5 files changed

+317
-12
lines changed

5 files changed

+317
-12
lines changed

.github/ISSUE_TEMPLATE/bug_report.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ assignees: ''
1010
**Describe the bug**
1111
A clear and concise description of what the bug is.
1212

13-
**To Reproduce**
13+
**How to reproduce**
1414
Steps to reproduce the behavior.
1515

1616
1. '...'

v2/domain/repository/elastic/transaction.go

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"log"
99
"sort"
10+
"strconv"
1011
"strings"
1112

1213
api "github.com/qubic/archive-query-service/v2/api/archive-query-service/v2"
@@ -166,9 +167,17 @@ func createIdentitiesQuery(identity string, filters map[string][]string, ranges
166167

167168
includeFilters, excludeFilters := splitFilters(filters)
168169

170+
// Check if there's an upper bound tickNumber range filter (lt/lte) and adjust if needed
171+
hasUpperBoundTickFilter, err := modifyUpperBoundTickNumberFilterIfNecessary(ranges, maxTick)
172+
if err != nil {
173+
return "", err
174+
}
175+
169176
filterStrings := make([]string, 0, len(includeFilters)+len(ranges)+1)
170-
// restrict to max tick (we don't care about potential duplicate tickNumber range filter)
171-
filterStrings = append(filterStrings, fmt.Sprintf(`{"range":{"tickNumber":{"lte":"%d"}}}`, maxTick))
177+
// restrict to max tick only if no upper bound tickNumber filter is present
178+
if !hasUpperBoundTickFilter {
179+
filterStrings = append(filterStrings, fmt.Sprintf(`{"range":{"tickNumber":{"lte":"%d"}}}`, maxTick))
180+
}
172181
// normal filters
173182
filterStrings = append(filterStrings, getFilterStrings(includeFilters)...)
174183
// append range filters
@@ -181,6 +190,15 @@ func createIdentitiesQuery(identity string, filters map[string][]string, ranges
181190
// filters for excluding results
182191
excludeFilterStrings := getFilterStrings(excludeFilters)
183192

193+
// filters are always present because we need to restrict to max tick
194+
filterQueryString := strings.Join(filterStrings, ",")
195+
196+
// only add exclude filters if present (otherwise empty)
197+
mustNotQueryString := strings.Join(excludeFilterStrings, ",")
198+
if len(mustNotQueryString) > 0 {
199+
mustNotQueryString = fmt.Sprintf(`, "must_not": [ %s ]`, mustNotQueryString)
200+
}
201+
184202
// in case we have a source or destination filter, the should clause still works
185203
query = `{
186204
"query": {
@@ -190,8 +208,7 @@ func createIdentitiesQuery(identity string, filters map[string][]string, ranges
190208
{ "term":{"destination":"%s"} }
191209
],
192210
"minimum_should_match": 1,
193-
"filter": [ %s ],
194-
"must_not": [ %s ]
211+
"filter": [ %s ] %s
195212
}
196213
},
197214
"sort": [ {"tickNumber":{"order":"desc"}} ],
@@ -201,11 +218,39 @@ func createIdentitiesQuery(identity string, filters map[string][]string, ranges
201218
}`
202219

203220
query = fmt.Sprintf(query, identity, identity,
204-
strings.Join(filterStrings, ","), strings.Join(excludeFilterStrings, ","),
221+
filterQueryString, mustNotQueryString,
205222
from, size, maxTrackTotalHits)
206223
return query, nil
207224
}
208225

226+
func modifyUpperBoundTickNumberFilterIfNecessary(ranges map[string][]*entities.Range, maxTick uint32) (bool, error) {
227+
hasUpperBoundTickFilter := false
228+
if tickRanges, ok := ranges["tickNumber"]; ok {
229+
for _, r := range tickRanges {
230+
if r.Operation == "lt" || r.Operation == "lte" {
231+
hasUpperBoundTickFilter = true
232+
// Parse the value and compare with maxTick
233+
tickValue, err := strconv.ParseUint(r.Value, 10, 32)
234+
if err != nil {
235+
return false, fmt.Errorf("parsing tickNumber range value: %w", err)
236+
}
237+
maxTickValue := If(r.Operation == "lte", maxTick, maxTick+1)
238+
if uint32(tickValue) > maxTickValue {
239+
r.Value = fmt.Sprintf("%d", maxTickValue)
240+
}
241+
}
242+
}
243+
}
244+
return hasUpperBoundTickFilter, nil
245+
}
246+
247+
func If[T any](cond bool, vtrue, vfalse T) T {
248+
if cond {
249+
return vtrue
250+
}
251+
return vfalse
252+
}
253+
209254
const excludeSuffix = "-exclude"
210255

211256
func splitFilters(filters map[string][]string) (map[string][]string, map[string][]string) {

v2/domain/repository/elastic/transaction_identities_query_test.go

Lines changed: 201 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ func Test_createIdentitiesQuery_returnQuery(t *testing.T) {
1919
{ "term":{"destination":"some-identity"} }
2020
],
2121
"minimum_should_match": 1,
22-
"filter": [{"range":{"tickNumber":{"lte":"12345"}}}],
23-
"must_not": []
22+
"filter": [{"range":{"tickNumber":{"lte":"12345"}}}]
2423
}
2524
},
2625
"sort": [ {"tickNumber":{"order":"desc"}} ],
@@ -49,8 +48,7 @@ func Test_createIdentitiesQuery_givenFilters_returnQueryWithFilters(t *testing.T
4948
{"range":{"tickNumber":{"lte":"1000000"}}},
5049
{"term":{"another-value":"foo"}},
5150
{"term":{"some-value":"42"}}
52-
],
53-
"must_not": []
51+
]
5452
}
5553
},
5654
"sort": [ {"tickNumber":{"order":"desc"}} ],
@@ -113,8 +111,7 @@ func Test_createIdentitiesQuery_givenRanges_returnQueryWithFilters(t *testing.T)
113111
{"range":{"another-value":{ "lte": "43", "gte": "12" }}},
114112
{"range":{"some-value":{ "lt": "42" }}},
115113
{"range":{"third-value":{ "gt": "44"}}}
116-
],
117-
"must_not": []
114+
]
118115
}
119116
},
120117
"sort": [ {"tickNumber":{"order":"desc"}} ],
@@ -171,3 +168,201 @@ func Test_createIdentitiesQuery_givenRangesAndFilters_returnQueryWithAllFilters(
171168
log.Println(query)
172169
require.JSONEq(t, expectedQuery, query)
173170
}
171+
172+
func Test_modifyUpperBoundTickNumberFilterIfNecessary_noRangeFilter(t *testing.T) {
173+
ranges := map[string][]*entities.Range{}
174+
maxTick := uint32(1000)
175+
176+
hasUpperBound, err := modifyUpperBoundTickNumberFilterIfNecessary(ranges, maxTick)
177+
require.NoError(t, err)
178+
require.False(t, hasUpperBound)
179+
}
180+
181+
func Test_modifyUpperBoundTickNumberFilterIfNecessary_upperBoundReplacedWithMaxTick(t *testing.T) {
182+
ranges := map[string][]*entities.Range{
183+
"tickNumber": {
184+
{Operation: "lte", Value: "5000"},
185+
},
186+
}
187+
maxTick := uint32(1000)
188+
189+
hasUpperBound, err := modifyUpperBoundTickNumberFilterIfNecessary(ranges, maxTick)
190+
require.NoError(t, err)
191+
require.True(t, hasUpperBound)
192+
require.Equal(t, "1000", ranges["tickNumber"][0].Value)
193+
}
194+
195+
func Test_modifyUpperBoundTickNumberFilterIfNecessary_upperBoundNotReplaced(t *testing.T) {
196+
ranges := map[string][]*entities.Range{
197+
"tickNumber": {
198+
{Operation: "lte", Value: "999"},
199+
},
200+
}
201+
maxTick := uint32(1000)
202+
203+
hasUpperBound, err := modifyUpperBoundTickNumberFilterIfNecessary(ranges, maxTick)
204+
require.NoError(t, err)
205+
require.True(t, hasUpperBound)
206+
require.Equal(t, "999", ranges["tickNumber"][0].Value)
207+
}
208+
209+
func Test_modifyUpperBoundTickNumberFilterIfNecessary_onlyLowerBound(t *testing.T) {
210+
ranges := map[string][]*entities.Range{
211+
"tickNumber": {
212+
{Operation: "gte", Value: "100"},
213+
},
214+
}
215+
maxTick := uint32(1000)
216+
217+
hasUpperBound, err := modifyUpperBoundTickNumberFilterIfNecessary(ranges, maxTick)
218+
require.NoError(t, err)
219+
require.False(t, hasUpperBound)
220+
require.Equal(t, "100", ranges["tickNumber"][0].Value)
221+
}
222+
223+
func Test_modifyUpperBoundTickNumberFilterIfNecessary_ltOperatorNotReplacedWithMaxTick(t *testing.T) {
224+
ranges := map[string][]*entities.Range{
225+
"tickNumber": {
226+
{Operation: "lt", Value: "1001"},
227+
},
228+
}
229+
maxTick := uint32(1000)
230+
231+
hasUpperBound, err := modifyUpperBoundTickNumberFilterIfNecessary(ranges, maxTick)
232+
require.NoError(t, err)
233+
require.True(t, hasUpperBound)
234+
require.Equal(t, "1001", ranges["tickNumber"][0].Value)
235+
}
236+
237+
func Test_modifyUpperBoundTickNumberFilterIfNecessary_ltOperatorReplacedWithMaxTick(t *testing.T) {
238+
ranges := map[string][]*entities.Range{
239+
"tickNumber": {
240+
{Operation: "lt", Value: "1002"},
241+
},
242+
}
243+
maxTick := uint32(1000)
244+
245+
hasUpperBound, err := modifyUpperBoundTickNumberFilterIfNecessary(ranges, maxTick)
246+
require.NoError(t, err)
247+
require.True(t, hasUpperBound)
248+
require.Equal(t, "1001", ranges["tickNumber"][0].Value)
249+
}
250+
251+
func Test_modifyUpperBoundTickNumberFilterIfNecessary_lteOperatorReplacedWithMaxTick(t *testing.T) {
252+
ranges := map[string][]*entities.Range{
253+
"tickNumber": {
254+
{Operation: "lte", Value: "5000"},
255+
},
256+
}
257+
maxTick := uint32(1000)
258+
259+
hasUpperBound, err := modifyUpperBoundTickNumberFilterIfNecessary(ranges, maxTick)
260+
require.NoError(t, err)
261+
require.True(t, hasUpperBound)
262+
require.Equal(t, "1000", ranges["tickNumber"][0].Value)
263+
}
264+
265+
func Test_modifyUpperBoundTickNumberFilterIfNecessary_invalidValueReturnsError(t *testing.T) {
266+
ranges := map[string][]*entities.Range{
267+
"tickNumber": {
268+
{Operation: "lte", Value: "not-a-number"},
269+
},
270+
}
271+
maxTick := uint32(1000)
272+
273+
hasUpperBound, err := modifyUpperBoundTickNumberFilterIfNecessary(ranges, maxTick)
274+
require.Error(t, err)
275+
require.False(t, hasUpperBound)
276+
require.Contains(t, err.Error(), "parsing tickNumber range value")
277+
}
278+
279+
func Test_createIdentitiesQuery_withTickNumberUpperBound_omitsMaxTickFilter(t *testing.T) {
280+
expectedQuery := `{
281+
"query": {
282+
"bool": {
283+
"should": [
284+
{ "term":{"source":"some-identity"} },
285+
{ "term":{"destination":"some-identity"} }
286+
],
287+
"minimum_should_match": 1,
288+
"filter": [
289+
{"range":{"tickNumber":{"lte":"500"}}}
290+
]
291+
}
292+
},
293+
"sort": [ {"tickNumber":{"order":"desc"}} ],
294+
"from": 0,
295+
"size": 10,
296+
"track_total_hits": 10000
297+
}`
298+
299+
ranges := map[string][]*entities.Range{
300+
"tickNumber": {{Operation: "lte", Value: "500"}},
301+
}
302+
query, err := createIdentitiesQuery(testIdentity, nil, ranges, 0, 10, 1000)
303+
require.NoError(t, err)
304+
require.NotEmpty(t, query)
305+
306+
require.JSONEq(t, expectedQuery, query)
307+
}
308+
309+
func Test_createIdentitiesQuery_withTickNumberUpperBoundExceedingMaxTick_replacesWithMaxTick(t *testing.T) {
310+
expectedQuery := `{
311+
"query": {
312+
"bool": {
313+
"should": [
314+
{ "term":{"source":"some-identity"} },
315+
{ "term":{"destination":"some-identity"} }
316+
],
317+
"minimum_should_match": 1,
318+
"filter": [
319+
{"range":{"tickNumber":{"lt":"1001"}}}
320+
]
321+
}
322+
},
323+
"sort": [ {"tickNumber":{"order":"desc"}} ],
324+
"from": 0,
325+
"size": 10,
326+
"track_total_hits": 10000
327+
}`
328+
329+
ranges := map[string][]*entities.Range{
330+
"tickNumber": {{Operation: "lt", Value: "5000"}},
331+
}
332+
query, err := createIdentitiesQuery(testIdentity, nil, ranges, 0, 10, 1000)
333+
require.NoError(t, err)
334+
require.NotEmpty(t, query)
335+
336+
require.JSONEq(t, expectedQuery, query)
337+
}
338+
339+
func Test_createIdentitiesQuery_withTickNumberLowerBoundOnly_includesMaxTickFilter(t *testing.T) {
340+
expectedQuery := `{
341+
"query": {
342+
"bool": {
343+
"should": [
344+
{ "term":{"source":"some-identity"} },
345+
{ "term":{"destination":"some-identity"} }
346+
],
347+
"minimum_should_match": 1,
348+
"filter": [
349+
{"range":{"tickNumber":{"lte":"1000"}}},
350+
{"range":{"tickNumber":{"gte":"100"}}}
351+
]
352+
}
353+
},
354+
"sort": [ {"tickNumber":{"order":"desc"}} ],
355+
"from": 0,
356+
"size": 10,
357+
"track_total_hits": 10000
358+
}`
359+
360+
ranges := map[string][]*entities.Range{
361+
"tickNumber": {{Operation: "gte", Value: "100"}},
362+
}
363+
query, err := createIdentitiesQuery(testIdentity, nil, ranges, 0, 10, 1000)
364+
require.NoError(t, err)
365+
require.NotEmpty(t, query)
366+
367+
require.JSONEq(t, expectedQuery, query)
368+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package elastic
2+
3+
import (
4+
"testing"
5+
6+
"github.com/qubic/archive-query-service/v2/entities"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
func Test_createRangeFilter(t *testing.T) {
11+
tests := []struct {
12+
name string
13+
property string
14+
ranges []*entities.Range
15+
want string
16+
}{
17+
{
18+
name: "valid single range",
19+
property: "tick",
20+
ranges: []*entities.Range{
21+
{Operation: "gte", Value: "100"},
22+
},
23+
want: `{"range":{"tick":{"gte":"100"}}}`,
24+
},
25+
{
26+
name: "valid dual range",
27+
property: "tick",
28+
ranges: []*entities.Range{
29+
{Operation: "gte", Value: "100"},
30+
{Operation: "lte", Value: "200"},
31+
},
32+
want: `{"range":{"tick":{"gte":"100","lte":"200"}}}`,
33+
},
34+
}
35+
for _, tt := range tests {
36+
t.Run(tt.name, func(t *testing.T) {
37+
got, err := createRangeFilter(tt.property, tt.ranges)
38+
require.NoError(t, err)
39+
require.JSONEq(t, tt.want, got)
40+
})
41+
}
42+
}

0 commit comments

Comments
 (0)