Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit e80dd00

Browse files
authoredSep 13, 2022
backport: performance improvements for the event query API (#7319) (#9334)
* Performance improvements for the event query API (#7319) Rework the implementation of event query parsing and execution to improve performance and reduce memory usage. Previous memory and CPU profiles of the pubsub service showed query processing as a significant hotspot. While we don't have evidence that this is visibly hurting users, fixing it is fairly easy and self-contained. Updates #6439. Typical benchmark results comparing the original implementation (PEG) with the reworked implementation (Custom): ``` TEST TIME/OP BYTES/OP ALLOCS/OP SPEEDUP MEM SAVING BenchmarkParsePEG-12 51716 ns 526832 27 BenchmarkParseCustom-12 2167 ns 4616 17 23.8x 99.1% BenchmarkMatchPEG-12 3086 ns 1097 22 BenchmarkMatchCustom-12 294.2 ns 64 3 10.5x 94.1% ```
1 parent 93ead3d commit e80dd00

32 files changed

+2300
-742
lines changed
 

‎CHANGELOG_PENDING.md

+22
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
11
# Unreleased Changes
22

3+
## v0.38.0
4+
5+
### BREAKING CHANGES
6+
7+
- CLI/RPC/Config
8+
9+
- Apps
10+
11+
- P2P Protocol
12+
13+
- Go API
14+
15+
- Blockchain Protocol
16+
17+
### FEATURES
18+
19+
### IMPROVEMENTS
20+
21+
- [pubsub] \#7319 Performance improvements for the event query API (@creachadair)
22+
23+
### BUG FIXES
24+
325
## v0.37.0
426

527
Special thanks to external contributors on this release:

‎Makefile

+1-1
Original file line numberDiff line numberDiff line change
@@ -406,4 +406,4 @@ $(BUILDDIR)/packages.txt:$(GO_TEST_FILES) $(BUILDDIR)
406406
split-test-packages:$(BUILDDIR)/packages.txt
407407
split -d -n l/$(NUM_SPLIT) $< $<.
408408
test-group-%:split-test-packages
409-
cat $(BUILDDIR)/packages.txt.$* | xargs go test -mod=readonly -timeout=5m -race -coverprofile=$(BUILDDIR)/$*.profile.out
409+
cat $(BUILDDIR)/packages.txt.$* | xargs go test -mod=readonly -timeout=15m -race -coverprofile=$(BUILDDIR)/$*.profile.out

‎libs/pubsub/example_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func TestExample(t *testing.T) {
2424
})
2525

2626
ctx := context.Background()
27-
subscription, err := s.Subscribe(ctx, "example-client", query.MustParse("abci.account.name='John'"))
27+
subscription, err := s.Subscribe(ctx, "example-client", query.MustCompile("abci.account.name='John'"))
2828
require.NoError(t, err)
2929
err = s.PublishWithEvents(ctx, "Tombstone", map[string][]string{"abci.account.name": {"John"}})
3030
require.NoError(t, err)

‎libs/pubsub/pubsub_test.go

+23-23
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func TestSubscribe(t *testing.T) {
3232
})
3333

3434
ctx := context.Background()
35-
subscription, err := s.Subscribe(ctx, clientID, query.Empty{})
35+
subscription, err := s.Subscribe(ctx, clientID, query.All)
3636
require.NoError(t, err)
3737

3838
assert.Equal(t, 1, s.NumClients())
@@ -78,14 +78,14 @@ func TestSubscribeWithCapacity(t *testing.T) {
7878

7979
ctx := context.Background()
8080
assert.Panics(t, func() {
81-
_, err = s.Subscribe(ctx, clientID, query.Empty{}, -1)
81+
_, err = s.Subscribe(ctx, clientID, query.All, -1)
8282
require.NoError(t, err)
8383
})
8484
assert.Panics(t, func() {
85-
_, err = s.Subscribe(ctx, clientID, query.Empty{}, 0)
85+
_, err = s.Subscribe(ctx, clientID, query.All, 0)
8686
require.NoError(t, err)
8787
})
88-
subscription, err := s.Subscribe(ctx, clientID, query.Empty{}, 1)
88+
subscription, err := s.Subscribe(ctx, clientID, query.All, 1)
8989
require.NoError(t, err)
9090
err = s.Publish(ctx, "Aggamon")
9191
require.NoError(t, err)
@@ -104,7 +104,7 @@ func TestSubscribeUnbuffered(t *testing.T) {
104104
})
105105

106106
ctx := context.Background()
107-
subscription, err := s.SubscribeUnbuffered(ctx, clientID, query.Empty{})
107+
subscription, err := s.SubscribeUnbuffered(ctx, clientID, query.All)
108108
require.NoError(t, err)
109109

110110
published := make(chan struct{})
@@ -139,7 +139,7 @@ func TestSlowClientIsRemovedWithErrOutOfCapacity(t *testing.T) {
139139
})
140140

141141
ctx := context.Background()
142-
subscription, err := s.Subscribe(ctx, clientID, query.Empty{})
142+
subscription, err := s.Subscribe(ctx, clientID, query.All)
143143
require.NoError(t, err)
144144
err = s.Publish(ctx, "Fat Cobra")
145145
require.NoError(t, err)
@@ -161,7 +161,7 @@ func TestDifferentClients(t *testing.T) {
161161
})
162162

163163
ctx := context.Background()
164-
subscription1, err := s.Subscribe(ctx, "client-1", query.MustParse("tm.events.type='NewBlock'"))
164+
subscription1, err := s.Subscribe(ctx, "client-1", query.MustCompile("tm.events.type='NewBlock'"))
165165
require.NoError(t, err)
166166
err = s.PublishWithEvents(ctx, "Iceman", map[string][]string{"tm.events.type": {"NewBlock"}})
167167
require.NoError(t, err)
@@ -170,7 +170,7 @@ func TestDifferentClients(t *testing.T) {
170170
subscription2, err := s.Subscribe(
171171
ctx,
172172
"client-2",
173-
query.MustParse("tm.events.type='NewBlock' AND abci.account.name='Igor'"),
173+
query.MustCompile("tm.events.type='NewBlock' AND abci.account.name='Igor'"),
174174
)
175175
require.NoError(t, err)
176176
err = s.PublishWithEvents(
@@ -185,7 +185,7 @@ func TestDifferentClients(t *testing.T) {
185185
subscription3, err := s.Subscribe(
186186
ctx,
187187
"client-3",
188-
query.MustParse("tm.events.type='NewRoundStep' AND abci.account.name='Igor' AND abci.invoice.number = 10"),
188+
query.MustCompile("tm.events.type='NewRoundStep' AND abci.account.name='Igor' AND abci.invoice.number = 10"),
189189
)
190190
require.NoError(t, err)
191191
err = s.PublishWithEvents(ctx, "Valeria Richards", map[string][]string{"tm.events.type": {"NewRoundStep"}})
@@ -227,7 +227,7 @@ func TestSubscribeDuplicateKeys(t *testing.T) {
227227
}
228228

229229
for i, tc := range testCases {
230-
sub, err := s.Subscribe(ctx, fmt.Sprintf("client-%d", i), query.MustParse(tc.query))
230+
sub, err := s.Subscribe(ctx, fmt.Sprintf("client-%d", i), query.MustCompile(tc.query))
231231
require.NoError(t, err)
232232

233233
err = s.PublishWithEvents(
@@ -260,7 +260,7 @@ func TestClientSubscribesTwice(t *testing.T) {
260260
})
261261

262262
ctx := context.Background()
263-
q := query.MustParse("tm.events.type='NewBlock'")
263+
q := query.MustCompile("tm.events.type='NewBlock'")
264264

265265
subscription1, err := s.Subscribe(ctx, clientID, q)
266266
require.NoError(t, err)
@@ -289,9 +289,9 @@ func TestUnsubscribe(t *testing.T) {
289289
})
290290

291291
ctx := context.Background()
292-
subscription, err := s.Subscribe(ctx, clientID, query.MustParse("tm.events.type='NewBlock'"))
292+
subscription, err := s.Subscribe(ctx, clientID, query.MustCompile("tm.events.type='NewBlock'"))
293293
require.NoError(t, err)
294-
err = s.Unsubscribe(ctx, clientID, query.MustParse("tm.events.type='NewBlock'"))
294+
err = s.Unsubscribe(ctx, clientID, query.MustCompile("tm.events.type='NewBlock'"))
295295
require.NoError(t, err)
296296

297297
err = s.Publish(ctx, "Nick Fury")
@@ -313,12 +313,12 @@ func TestClientUnsubscribesTwice(t *testing.T) {
313313
})
314314

315315
ctx := context.Background()
316-
_, err = s.Subscribe(ctx, clientID, query.MustParse("tm.events.type='NewBlock'"))
316+
_, err = s.Subscribe(ctx, clientID, query.MustCompile("tm.events.type='NewBlock'"))
317317
require.NoError(t, err)
318-
err = s.Unsubscribe(ctx, clientID, query.MustParse("tm.events.type='NewBlock'"))
318+
err = s.Unsubscribe(ctx, clientID, query.MustCompile("tm.events.type='NewBlock'"))
319319
require.NoError(t, err)
320320

321-
err = s.Unsubscribe(ctx, clientID, query.MustParse("tm.events.type='NewBlock'"))
321+
err = s.Unsubscribe(ctx, clientID, query.MustCompile("tm.events.type='NewBlock'"))
322322
assert.Equal(t, pubsub.ErrSubscriptionNotFound, err)
323323
err = s.UnsubscribeAll(ctx, clientID)
324324
assert.Equal(t, pubsub.ErrSubscriptionNotFound, err)
@@ -336,11 +336,11 @@ func TestResubscribe(t *testing.T) {
336336
})
337337

338338
ctx := context.Background()
339-
_, err = s.Subscribe(ctx, clientID, query.Empty{})
339+
_, err = s.Subscribe(ctx, clientID, query.All)
340340
require.NoError(t, err)
341-
err = s.Unsubscribe(ctx, clientID, query.Empty{})
341+
err = s.Unsubscribe(ctx, clientID, query.All)
342342
require.NoError(t, err)
343-
subscription, err := s.Subscribe(ctx, clientID, query.Empty{})
343+
subscription, err := s.Subscribe(ctx, clientID, query.All)
344344
require.NoError(t, err)
345345

346346
err = s.Publish(ctx, "Cable")
@@ -360,9 +360,9 @@ func TestUnsubscribeAll(t *testing.T) {
360360
})
361361

362362
ctx := context.Background()
363-
subscription1, err := s.Subscribe(ctx, clientID, query.MustParse("tm.events.type='NewBlock'"))
363+
subscription1, err := s.Subscribe(ctx, clientID, query.MustCompile("tm.events.type='NewBlock'"))
364364
require.NoError(t, err)
365-
subscription2, err := s.Subscribe(ctx, clientID, query.MustParse("tm.events.type='NewBlockHeader'"))
365+
subscription2, err := s.Subscribe(ctx, clientID, query.MustCompile("tm.events.type='NewBlockHeader'"))
366366
require.NoError(t, err)
367367

368368
err = s.UnsubscribeAll(ctx, clientID)
@@ -421,7 +421,7 @@ func benchmarkNClients(n int, b *testing.B) {
421421
subscription, err := s.Subscribe(
422422
ctx,
423423
clientID,
424-
query.MustParse(fmt.Sprintf("abci.Account.Owner = 'Ivan' AND abci.Invoices.Number = %d", i)),
424+
query.MustCompile(fmt.Sprintf("abci.Account.Owner = 'Ivan' AND abci.Invoices.Number = %d", i)),
425425
)
426426
if err != nil {
427427
b.Fatal(err)
@@ -461,7 +461,7 @@ func benchmarkNClientsOneQuery(n int, b *testing.B) {
461461
})
462462

463463
ctx := context.Background()
464-
q := query.MustParse("abci.Account.Owner = 'Ivan' AND abci.Invoices.Number = 1")
464+
q := query.MustCompile("abci.Account.Owner = 'Ivan' AND abci.Invoices.Number = 1")
465465
for i := 0; i < n; i++ {
466466
subscription, err := s.Subscribe(ctx, clientID, q)
467467
if err != nil {

‎libs/pubsub/query/bench_test.go

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package query_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/tendermint/tendermint/libs/pubsub/query"
7+
oldquery "github.com/tendermint/tendermint/libs/pubsub/query/oldquery"
8+
)
9+
10+
const testQuery = `tm.events.type='NewBlock' AND abci.account.name='Igor'`
11+
12+
var testEvents = map[string][]string{
13+
"tm.events.index": {
14+
"25",
15+
},
16+
"tm.events.type": {
17+
"NewBlock",
18+
},
19+
"abci.account.name": {
20+
"Anya", "Igor",
21+
},
22+
}
23+
24+
func BenchmarkParsePEG(b *testing.B) {
25+
for i := 0; i < b.N; i++ {
26+
_, err := oldquery.New(testQuery)
27+
if err != nil {
28+
b.Fatal(err)
29+
}
30+
}
31+
}
32+
33+
func BenchmarkParseCustom(b *testing.B) {
34+
for i := 0; i < b.N; i++ {
35+
_, err := query.New(testQuery)
36+
if err != nil {
37+
b.Fatal(err)
38+
}
39+
}
40+
}
41+
42+
func BenchmarkMatchPEG(b *testing.B) {
43+
q, err := oldquery.New(testQuery)
44+
if err != nil {
45+
b.Fatal(err)
46+
}
47+
b.ResetTimer()
48+
for i := 0; i < b.N; i++ {
49+
ok, err := q.Matches(testEvents)
50+
if err != nil {
51+
b.Fatal(err)
52+
} else if !ok {
53+
b.Error("no match")
54+
}
55+
}
56+
}
57+
58+
func BenchmarkMatchCustom(b *testing.B) {
59+
q, err := query.New(testQuery)
60+
if err != nil {
61+
b.Fatal(err)
62+
}
63+
b.ResetTimer()
64+
for i := 0; i < b.N; i++ {
65+
ok, err := q.Matches(testEvents)
66+
if err != nil {
67+
b.Fatal(err)
68+
} else if !ok {
69+
b.Error("no match")
70+
}
71+
}
72+
}

‎libs/pubsub/query/Makefile ‎libs/pubsub/query/oldquery/Makefile

+3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
gen_query_parser:
2+
go generate .
3+
14
fuzzy_test:
25
go get -u -v github.com/dvyukov/go-fuzz/go-fuzz
36
go get -u -v github.com/dvyukov/go-fuzz/go-fuzz-build
File renamed without changes.

‎libs/pubsub/query/empty_test.go ‎libs/pubsub/query/oldquery/empty_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55

66
"github.com/stretchr/testify/assert"
77

8-
"github.com/tendermint/tendermint/libs/pubsub/query"
8+
query "github.com/tendermint/tendermint/libs/pubsub/query/oldquery"
99
)
1010

1111
func TestEmptyQueryMatchesAnything(t *testing.T) {

‎libs/pubsub/query/fuzz_test/main.go ‎libs/pubsub/query/oldquery/fuzz_test/main.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package fuzz_test
33
import (
44
"fmt"
55

6-
"github.com/tendermint/tendermint/libs/pubsub/query"
6+
query "github.com/tendermint/tendermint/libs/pubsub/query/oldquery"
77
)
88

99
func Fuzz(data []byte) int {

‎libs/pubsub/query/parser_test.go ‎libs/pubsub/query/oldquery/parser_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import (
55

66
"github.com/stretchr/testify/assert"
77

8-
"github.com/tendermint/tendermint/libs/pubsub/query"
8+
query "github.com/tendermint/tendermint/libs/pubsub/query/oldquery"
99
)
1010

1111
// TODO: fuzzy testing?

‎libs/pubsub/query/oldquery/peg.go

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package query
2+
3+
//go:generate go run github.com/pointlander/peg@v1.0.0 -inline -switch query.peg

‎libs/pubsub/query/oldquery/query.go

+504
Large diffs are not rendered by default.
File renamed without changes.
File renamed without changes.
+180
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package query_test
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
"time"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
query "github.com/tendermint/tendermint/libs/pubsub/query/oldquery"
11+
)
12+
13+
func TestMatches(t *testing.T) {
14+
var (
15+
txDate = "2017-01-01"
16+
txTime = "2018-05-03T14:45:00Z"
17+
)
18+
19+
testCases := []struct {
20+
s string
21+
events map[string][]string
22+
matches bool
23+
}{
24+
{"tm.events.type='NewBlock'", map[string][]string{"tm.events.type": {"NewBlock"}}, true},
25+
{"tx.gas > 7", map[string][]string{"tx.gas": {"8"}}, true},
26+
{"transfer.amount > 7", map[string][]string{"transfer.amount": {"8stake"}}, true},
27+
{"transfer.amount > 7", map[string][]string{"transfer.amount": {"8.045stake"}}, true},
28+
{"transfer.amount > 7.043", map[string][]string{"transfer.amount": {"8.045stake"}}, true},
29+
{"transfer.amount > 8.045", map[string][]string{"transfer.amount": {"8.045stake"}}, false},
30+
{"tx.gas > 7 AND tx.gas < 9", map[string][]string{"tx.gas": {"8"}}, true},
31+
{"body.weight >= 3.5", map[string][]string{"body.weight": {"3.5"}}, true},
32+
{"account.balance < 1000.0", map[string][]string{"account.balance": {"900"}}, true},
33+
{"apples.kg <= 4", map[string][]string{"apples.kg": {"4.0"}}, true},
34+
{"body.weight >= 4.5", map[string][]string{"body.weight": {fmt.Sprintf("%v", float32(4.5))}}, true},
35+
{
36+
"oranges.kg < 4 AND watermellons.kg > 10",
37+
map[string][]string{"oranges.kg": {"3"}, "watermellons.kg": {"12"}},
38+
true,
39+
},
40+
{"peaches.kg < 4", map[string][]string{"peaches.kg": {"5"}}, false},
41+
{
42+
"tx.date > DATE 2017-01-01",
43+
map[string][]string{"tx.date": {time.Now().Format(query.DateLayout)}},
44+
true,
45+
},
46+
{"tx.date = DATE 2017-01-01", map[string][]string{"tx.date": {txDate}}, true},
47+
{"tx.date = DATE 2018-01-01", map[string][]string{"tx.date": {txDate}}, false},
48+
{
49+
"tx.time >= TIME 2013-05-03T14:45:00Z",
50+
map[string][]string{"tx.time": {time.Now().Format(query.TimeLayout)}},
51+
true,
52+
},
53+
{"tx.time = TIME 2013-05-03T14:45:00Z", map[string][]string{"tx.time": {txTime}}, false},
54+
{"abci.owner.name CONTAINS 'Igor'", map[string][]string{"abci.owner.name": {"Igor,Ivan"}}, true},
55+
{"abci.owner.name CONTAINS 'Igor'", map[string][]string{"abci.owner.name": {"Pavel,Ivan"}}, false},
56+
{"abci.owner.name = 'Igor'", map[string][]string{"abci.owner.name": {"Igor", "Ivan"}}, true},
57+
{
58+
"abci.owner.name = 'Ivan'",
59+
map[string][]string{"abci.owner.name": {"Igor", "Ivan"}},
60+
true,
61+
},
62+
{
63+
"abci.owner.name = 'Ivan' AND abci.owner.name = 'Igor'",
64+
map[string][]string{"abci.owner.name": {"Igor", "Ivan"}},
65+
true,
66+
},
67+
{
68+
"abci.owner.name = 'Ivan' AND abci.owner.name = 'John'",
69+
map[string][]string{"abci.owner.name": {"Igor", "Ivan"}},
70+
false,
71+
},
72+
{
73+
"tm.events.type='NewBlock'",
74+
map[string][]string{"tm.events.type": {"NewBlock"}, "app.name": {"fuzzed"}},
75+
true,
76+
},
77+
{
78+
"app.name = 'fuzzed'",
79+
map[string][]string{"tm.events.type": {"NewBlock"}, "app.name": {"fuzzed"}},
80+
true,
81+
},
82+
{
83+
"tm.events.type='NewBlock' AND app.name = 'fuzzed'",
84+
map[string][]string{"tm.events.type": {"NewBlock"}, "app.name": {"fuzzed"}},
85+
true,
86+
},
87+
{
88+
"tm.events.type='NewHeader' AND app.name = 'fuzzed'",
89+
map[string][]string{"tm.events.type": {"NewBlock"}, "app.name": {"fuzzed"}},
90+
false,
91+
},
92+
{"slash EXISTS",
93+
map[string][]string{"slash.reason": {"missing_signature"}, "slash.power": {"6000"}},
94+
true,
95+
},
96+
{"sl EXISTS",
97+
map[string][]string{"slash.reason": {"missing_signature"}, "slash.power": {"6000"}},
98+
true,
99+
},
100+
{"slash EXISTS",
101+
map[string][]string{"transfer.recipient": {"cosmos1gu6y2a0ffteesyeyeesk23082c6998xyzmt9mz"},
102+
"transfer.sender": {"cosmos1crje20aj4gxdtyct7z3knxqry2jqt2fuaey6u5"}},
103+
false,
104+
},
105+
{"slash.reason EXISTS AND slash.power > 1000",
106+
map[string][]string{"slash.reason": {"missing_signature"}, "slash.power": {"6000"}},
107+
true,
108+
},
109+
{"slash.reason EXISTS AND slash.power > 1000",
110+
map[string][]string{"slash.reason": {"missing_signature"}, "slash.power": {"500"}},
111+
false,
112+
},
113+
{"slash.reason EXISTS",
114+
map[string][]string{"transfer.recipient": {"cosmos1gu6y2a0ffteesyeyeesk23082c6998xyzmt9mz"},
115+
"transfer.sender": {"cosmos1crje20aj4gxdtyct7z3knxqry2jqt2fuaey6u5"}},
116+
false,
117+
},
118+
}
119+
120+
for _, tc := range testCases {
121+
q, err := query.New(tc.s)
122+
require.Nil(t, err)
123+
require.NotNil(t, q, "Query '%s' should not be nil", tc.s)
124+
125+
match, err := q.Matches(tc.events)
126+
require.Nil(t, err, "Query '%s' should not error on input %v", tc.s, tc.events)
127+
require.Equal(t, tc.matches, match, "Query '%s' on input %v: got %v, want %v",
128+
tc.s, tc.events, match, tc.matches)
129+
}
130+
}
131+
132+
func TestMustParse(t *testing.T) {
133+
assert.Panics(t, func() { query.MustParse("=") })
134+
assert.NotPanics(t, func() { query.MustParse("tm.events.type='NewBlock'") })
135+
}
136+
137+
func TestConditions(t *testing.T) {
138+
txTime, err := time.Parse(time.RFC3339, "2013-05-03T14:45:00Z")
139+
require.NoError(t, err)
140+
141+
testCases := []struct {
142+
s string
143+
conditions []query.Condition
144+
}{
145+
{
146+
s: "tm.events.type='NewBlock'",
147+
conditions: []query.Condition{
148+
{CompositeKey: "tm.events.type", Op: query.OpEqual, Operand: "NewBlock"},
149+
},
150+
},
151+
{
152+
s: "tx.gas > 7 AND tx.gas < 9",
153+
conditions: []query.Condition{
154+
{CompositeKey: "tx.gas", Op: query.OpGreater, Operand: int64(7)},
155+
{CompositeKey: "tx.gas", Op: query.OpLess, Operand: int64(9)},
156+
},
157+
},
158+
{
159+
s: "tx.time >= TIME 2013-05-03T14:45:00Z",
160+
conditions: []query.Condition{
161+
{CompositeKey: "tx.time", Op: query.OpGreaterEqual, Operand: txTime},
162+
},
163+
},
164+
{
165+
s: "slashing EXISTS",
166+
conditions: []query.Condition{
167+
{CompositeKey: "slashing", Op: query.OpExists},
168+
},
169+
},
170+
}
171+
172+
for _, tc := range testCases {
173+
q, err := query.New(tc.s)
174+
require.Nil(t, err)
175+
176+
c, err := q.Conditions()
177+
require.NoError(t, err)
178+
assert.Equal(t, tc.conditions, c)
179+
}
180+
}

‎libs/pubsub/query/peg.go

-10
This file was deleted.

‎libs/pubsub/query/query.go

+281-438
Large diffs are not rendered by default.

‎libs/pubsub/query/query_test.go

+371-186
Large diffs are not rendered by default.

‎libs/pubsub/query/syntax/doc.go

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Package syntax defines a scanner and parser for the Tendermint event filter
2+
// query language. A query selects events by their types and attribute values.
3+
//
4+
// # Grammar
5+
//
6+
// The grammar of the query language is defined by the following EBNF:
7+
//
8+
// query = conditions EOF
9+
// conditions = condition {"AND" condition}
10+
// condition = tag comparison
11+
// comparison = equal / order / contains / "EXISTS"
12+
// equal = "=" (date / number / time / value)
13+
// order = cmp (date / number / time)
14+
// contains = "CONTAINS" value
15+
// cmp = "<" / "<=" / ">" / ">="
16+
//
17+
// The lexical terms are defined here using RE2 regular expression notation:
18+
//
19+
// // The name of an event attribute (type.value)
20+
// tag = #'\w+(\.\w+)*'
21+
//
22+
// // A datestamp (YYYY-MM-DD)
23+
// date = #'DATE \d{4}-\d{2}-\d{2}'
24+
//
25+
// // A number with optional fractional parts (0, 10, 3.25)
26+
// number = #'\d+(\.\d+)?'
27+
//
28+
// // An RFC3339 timestamp (2021-11-23T22:04:19-09:00)
29+
// time = #'TIME \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([-+]\d{2}:\d{2}|Z)'
30+
//
31+
// // A quoted literal string value ('a b c')
32+
// value = #'\'[^\']*\''
33+
package syntax

‎libs/pubsub/query/syntax/parser.go

+213
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
package syntax
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"math"
7+
"strconv"
8+
"strings"
9+
"time"
10+
)
11+
12+
// Parse parses the specified query string. It is shorthand for constructing a
13+
// parser for s and calling its Parse method.
14+
func Parse(s string) (Query, error) {
15+
return NewParser(strings.NewReader(s)).Parse()
16+
}
17+
18+
// Query is the root of the parse tree for a query. A query is the conjunction
19+
// of one or more conditions.
20+
type Query []Condition
21+
22+
func (q Query) String() string {
23+
ss := make([]string, len(q))
24+
for i, cond := range q {
25+
ss[i] = cond.String()
26+
}
27+
return strings.Join(ss, " AND ")
28+
}
29+
30+
// A Condition is a single conditional expression, consisting of a tag, a
31+
// comparison operator, and an optional argument. The type of the argument
32+
// depends on the operator.
33+
type Condition struct {
34+
Tag string
35+
Op Token
36+
Arg *Arg
37+
38+
opText string
39+
}
40+
41+
func (c Condition) String() string {
42+
s := c.Tag + " " + c.opText
43+
if c.Arg != nil {
44+
return s + " " + c.Arg.String()
45+
}
46+
return s
47+
}
48+
49+
// An Arg is the argument of a comparison operator.
50+
type Arg struct {
51+
Type Token
52+
text string
53+
}
54+
55+
func (a *Arg) String() string {
56+
if a == nil {
57+
return ""
58+
}
59+
switch a.Type {
60+
case TString:
61+
return "'" + a.text + "'"
62+
case TTime:
63+
return "TIME " + a.text
64+
case TDate:
65+
return "DATE " + a.text
66+
default:
67+
return a.text
68+
}
69+
}
70+
71+
// Number returns the value of the argument text as a number, or a NaN if the
72+
// text does not encode a valid number value.
73+
func (a *Arg) Number() float64 {
74+
if a == nil {
75+
return -1
76+
}
77+
v, err := strconv.ParseFloat(a.text, 64)
78+
if err == nil && v >= 0 {
79+
return v
80+
}
81+
return math.NaN()
82+
}
83+
84+
// Time returns the value of the argument text as a time, or the zero value if
85+
// the text does not encode a timestamp or datestamp.
86+
func (a *Arg) Time() time.Time {
87+
var ts time.Time
88+
if a == nil {
89+
return ts
90+
}
91+
var err error
92+
switch a.Type {
93+
case TDate:
94+
ts, err = ParseDate(a.text)
95+
case TTime:
96+
ts, err = ParseTime(a.text)
97+
}
98+
if err == nil {
99+
return ts
100+
}
101+
return time.Time{}
102+
}
103+
104+
// Value returns the value of the argument text as a string, or "".
105+
func (a *Arg) Value() string {
106+
if a == nil {
107+
return ""
108+
}
109+
return a.text
110+
}
111+
112+
// Parser is a query expression parser. The grammar for query expressions is
113+
// defined in the syntax package documentation.
114+
type Parser struct {
115+
scanner *Scanner
116+
}
117+
118+
// NewParser constructs a new parser that reads the input from r.
119+
func NewParser(r io.Reader) *Parser {
120+
return &Parser{scanner: NewScanner(r)}
121+
}
122+
123+
// Parse parses the complete input and returns the resulting query.
124+
func (p *Parser) Parse() (Query, error) {
125+
cond, err := p.parseCond()
126+
if err != nil {
127+
return nil, err
128+
}
129+
conds := []Condition{cond}
130+
for p.scanner.Next() != io.EOF {
131+
if tok := p.scanner.Token(); tok != TAnd {
132+
return nil, fmt.Errorf("offset %d: got %v, want %v", p.scanner.Pos(), tok, TAnd)
133+
}
134+
cond, err := p.parseCond()
135+
if err != nil {
136+
return nil, err
137+
}
138+
conds = append(conds, cond)
139+
}
140+
return conds, nil
141+
}
142+
143+
// parseCond parses a conditional expression: tag OP value.
144+
func (p *Parser) parseCond() (Condition, error) {
145+
var cond Condition
146+
if err := p.require(TTag); err != nil {
147+
return cond, err
148+
}
149+
cond.Tag = p.scanner.Text()
150+
if err := p.require(TLeq, TGeq, TLt, TGt, TEq, TContains, TExists); err != nil {
151+
return cond, err
152+
}
153+
cond.Op = p.scanner.Token()
154+
cond.opText = p.scanner.Text()
155+
156+
var err error
157+
switch cond.Op {
158+
case TLeq, TGeq, TLt, TGt:
159+
err = p.require(TNumber, TTime, TDate)
160+
case TEq:
161+
err = p.require(TNumber, TTime, TDate, TString)
162+
case TContains:
163+
err = p.require(TString)
164+
case TExists:
165+
// no argument
166+
return cond, nil
167+
default:
168+
return cond, fmt.Errorf("offset %d: unexpected operator %v", p.scanner.Pos(), cond.Op)
169+
}
170+
if err != nil {
171+
return cond, err
172+
}
173+
cond.Arg = &Arg{Type: p.scanner.Token(), text: p.scanner.Text()}
174+
return cond, nil
175+
}
176+
177+
// require advances the scanner and requires that the resulting token is one of
178+
// the specified token types.
179+
func (p *Parser) require(tokens ...Token) error {
180+
if err := p.scanner.Next(); err != nil {
181+
return fmt.Errorf("offset %d: %w", p.scanner.Pos(), err)
182+
}
183+
got := p.scanner.Token()
184+
for _, tok := range tokens {
185+
if tok == got {
186+
return nil
187+
}
188+
}
189+
return fmt.Errorf("offset %d: got %v, wanted %s", p.scanner.Pos(), got, tokLabel(tokens))
190+
}
191+
192+
// tokLabel makes a human-readable summary string for the given token types.
193+
func tokLabel(tokens []Token) string {
194+
if len(tokens) == 1 {
195+
return tokens[0].String()
196+
}
197+
last := len(tokens) - 1
198+
ss := make([]string, len(tokens)-1)
199+
for i, tok := range tokens[:last] {
200+
ss[i] = tok.String()
201+
}
202+
return strings.Join(ss, ", ") + " or " + tokens[last].String()
203+
}
204+
205+
// ParseDate parses s as a date string in the format used by DATE values.
206+
func ParseDate(s string) (time.Time, error) {
207+
return time.Parse("2006-01-02", s)
208+
}
209+
210+
// ParseTime parses s as a timestamp in the format used by TIME values.
211+
func ParseTime(s string) (time.Time, error) {
212+
return time.Parse(time.RFC3339, s)
213+
}

‎libs/pubsub/query/syntax/scanner.go

+312
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,312 @@
1+
package syntax
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"fmt"
7+
"io"
8+
"strings"
9+
"time"
10+
"unicode"
11+
)
12+
13+
// Token is the type of a lexical token in the query grammar.
14+
type Token byte
15+
16+
const (
17+
TInvalid = iota // invalid or unknown token
18+
TTag // field tag: x.y
19+
TString // string value: 'foo bar'
20+
TNumber // number: 0, 15.5, 100
21+
TTime // timestamp: TIME yyyy-mm-ddThh:mm:ss([-+]hh:mm|Z)
22+
TDate // datestamp: DATE yyyy-mm-dd
23+
TAnd // operator: AND
24+
TContains // operator: CONTAINS
25+
TExists // operator: EXISTS
26+
TEq // operator: =
27+
TLt // operator: <
28+
TLeq // operator: <=
29+
TGt // operator: >
30+
TGeq // operator: >=
31+
32+
// Do not reorder these values without updating the scanner code.
33+
)
34+
35+
var tString = [...]string{
36+
TInvalid: "invalid token",
37+
TTag: "tag",
38+
TString: "string",
39+
TNumber: "number",
40+
TTime: "timestamp",
41+
TDate: "datestamp",
42+
TAnd: "AND operator",
43+
TContains: "CONTAINS operator",
44+
TExists: "EXISTS operator",
45+
TEq: "= operator",
46+
TLt: "< operator",
47+
TLeq: "<= operator",
48+
TGt: "> operator",
49+
TGeq: ">= operator",
50+
}
51+
52+
func (t Token) String() string {
53+
v := int(t)
54+
if v > len(tString) {
55+
return "unknown token type"
56+
}
57+
return tString[v]
58+
}
59+
60+
const (
61+
// TimeFormat is the format string used for timestamp values.
62+
TimeFormat = time.RFC3339
63+
64+
// DateFormat is the format string used for datestamp values.
65+
DateFormat = "2006-01-02"
66+
)
67+
68+
// Scanner reads lexical tokens of the query language from an input stream.
69+
// Each call to Next advances the scanner to the next token, or reports an
70+
// error.
71+
type Scanner struct {
72+
r *bufio.Reader
73+
buf bytes.Buffer
74+
tok Token
75+
err error
76+
77+
pos, last, end int
78+
}
79+
80+
// NewScanner constructs a new scanner that reads from r.
81+
func NewScanner(r io.Reader) *Scanner { return &Scanner{r: bufio.NewReader(r)} }
82+
83+
// Next advances s to the next token in the input, or reports an error. At the
84+
// end of input, Next returns io.EOF.
85+
func (s *Scanner) Next() error {
86+
s.buf.Reset()
87+
s.pos = s.end
88+
s.tok = TInvalid
89+
s.err = nil
90+
91+
for {
92+
ch, err := s.rune()
93+
if err != nil {
94+
return s.fail(err)
95+
}
96+
if unicode.IsSpace(ch) {
97+
s.pos = s.end
98+
continue // skip whitespace
99+
}
100+
if '0' <= ch && ch <= '9' {
101+
return s.scanNumber(ch)
102+
} else if isTagRune(ch) {
103+
return s.scanTagLike(ch)
104+
}
105+
switch ch {
106+
case '\'':
107+
return s.scanString(ch)
108+
case '<', '>', '=':
109+
return s.scanCompare(ch)
110+
default:
111+
return s.invalid(ch)
112+
}
113+
}
114+
}
115+
116+
// Token returns the type of the current input token.
117+
func (s *Scanner) Token() Token { return s.tok }
118+
119+
// Text returns the text of the current input token.
120+
func (s *Scanner) Text() string { return s.buf.String() }
121+
122+
// Pos returns the start offset of the current token in the input.
123+
func (s *Scanner) Pos() int { return s.pos }
124+
125+
// Err returns the last error reported by Next, if any.
126+
func (s *Scanner) Err() error { return s.err }
127+
128+
// scanNumber scans for numbers with optional fractional parts.
129+
// Examples: 0, 1, 3.14
130+
func (s *Scanner) scanNumber(first rune) error {
131+
s.buf.WriteRune(first)
132+
if err := s.scanWhile(isDigit); err != nil {
133+
return err
134+
}
135+
136+
ch, err := s.rune()
137+
if err != nil && err != io.EOF {
138+
return err
139+
}
140+
if ch == '.' {
141+
s.buf.WriteRune(ch)
142+
if err := s.scanWhile(isDigit); err != nil {
143+
return err
144+
}
145+
} else {
146+
s.unrune()
147+
}
148+
s.tok = TNumber
149+
return nil
150+
}
151+
152+
func (s *Scanner) scanString(first rune) error {
153+
// discard opening quote
154+
for {
155+
ch, err := s.rune()
156+
if err != nil {
157+
return s.fail(err)
158+
} else if ch == first {
159+
// discard closing quote
160+
s.tok = TString
161+
return nil
162+
}
163+
s.buf.WriteRune(ch)
164+
}
165+
}
166+
167+
func (s *Scanner) scanCompare(first rune) error {
168+
s.buf.WriteRune(first)
169+
switch first {
170+
case '=':
171+
s.tok = TEq
172+
return nil
173+
case '<':
174+
s.tok = TLt
175+
case '>':
176+
s.tok = TGt
177+
default:
178+
return s.invalid(first)
179+
}
180+
181+
ch, err := s.rune()
182+
if err == io.EOF {
183+
return nil // the assigned token is correct
184+
} else if err != nil {
185+
return s.fail(err)
186+
}
187+
if ch == '=' {
188+
s.buf.WriteRune(ch)
189+
s.tok++ // depends on token order
190+
return nil
191+
}
192+
s.unrune()
193+
return nil
194+
}
195+
196+
func (s *Scanner) scanTagLike(first rune) error {
197+
s.buf.WriteRune(first)
198+
var hasSpace bool
199+
for {
200+
ch, err := s.rune()
201+
if err == io.EOF {
202+
break
203+
} else if err != nil {
204+
return s.fail(err)
205+
}
206+
if !isTagRune(ch) {
207+
hasSpace = ch == ' ' // to check for TIME, DATE
208+
break
209+
}
210+
s.buf.WriteRune(ch)
211+
}
212+
213+
text := s.buf.String()
214+
switch text {
215+
case "TIME":
216+
if hasSpace {
217+
return s.scanTimestamp()
218+
}
219+
s.tok = TTag
220+
case "DATE":
221+
if hasSpace {
222+
return s.scanDatestamp()
223+
}
224+
s.tok = TTag
225+
case "AND":
226+
s.tok = TAnd
227+
case "EXISTS":
228+
s.tok = TExists
229+
case "CONTAINS":
230+
s.tok = TContains
231+
default:
232+
s.tok = TTag
233+
}
234+
s.unrune()
235+
return nil
236+
}
237+
238+
func (s *Scanner) scanTimestamp() error {
239+
s.buf.Reset() // discard "TIME" label
240+
if err := s.scanWhile(isTimeRune); err != nil {
241+
return err
242+
}
243+
if ts, err := time.Parse(TimeFormat, s.buf.String()); err != nil {
244+
return s.fail(fmt.Errorf("invalid TIME value: %w", err))
245+
} else if y := ts.Year(); y < 1900 || y > 2999 {
246+
return s.fail(fmt.Errorf("timestamp year %d out of range", ts.Year()))
247+
}
248+
s.tok = TTime
249+
return nil
250+
}
251+
252+
func (s *Scanner) scanDatestamp() error {
253+
s.buf.Reset() // discard "DATE" label
254+
if err := s.scanWhile(isDateRune); err != nil {
255+
return err
256+
}
257+
if ts, err := time.Parse(DateFormat, s.buf.String()); err != nil {
258+
return s.fail(fmt.Errorf("invalid DATE value: %w", err))
259+
} else if y := ts.Year(); y < 1900 || y > 2999 {
260+
return s.fail(fmt.Errorf("datestamp year %d out of range", ts.Year()))
261+
}
262+
s.tok = TDate
263+
return nil
264+
}
265+
266+
func (s *Scanner) scanWhile(ok func(rune) bool) error {
267+
for {
268+
ch, err := s.rune()
269+
if err == io.EOF {
270+
return nil
271+
} else if err != nil {
272+
return s.fail(err)
273+
} else if !ok(ch) {
274+
s.unrune()
275+
return nil
276+
}
277+
s.buf.WriteRune(ch)
278+
}
279+
}
280+
281+
func (s *Scanner) rune() (rune, error) {
282+
ch, nb, err := s.r.ReadRune()
283+
s.last = nb
284+
s.end += nb
285+
return ch, err
286+
}
287+
288+
func (s *Scanner) unrune() {
289+
_ = s.r.UnreadRune()
290+
s.end -= s.last
291+
}
292+
293+
func (s *Scanner) fail(err error) error {
294+
s.err = err
295+
return err
296+
}
297+
298+
func (s *Scanner) invalid(ch rune) error {
299+
return s.fail(fmt.Errorf("invalid input %c at offset %d", ch, s.end))
300+
}
301+
302+
func isDigit(r rune) bool { return '0' <= r && r <= '9' }
303+
304+
func isTagRune(r rune) bool {
305+
return r == '.' || r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r)
306+
}
307+
308+
func isTimeRune(r rune) bool {
309+
return strings.ContainsRune("-T:+Z", r) || isDigit(r)
310+
}
311+
312+
func isDateRune(r rune) bool { return isDigit(r) || r == '-' }
+190
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
package syntax_test
2+
3+
import (
4+
"io"
5+
"reflect"
6+
"strings"
7+
"testing"
8+
9+
"github.com/tendermint/tendermint/libs/pubsub/query/syntax"
10+
)
11+
12+
func TestScanner(t *testing.T) {
13+
tests := []struct {
14+
input string
15+
want []syntax.Token
16+
}{
17+
// Empty inputs
18+
{"", nil},
19+
{" ", nil},
20+
{"\t\n ", nil},
21+
22+
// Numbers
23+
{`0 123`, []syntax.Token{syntax.TNumber, syntax.TNumber}},
24+
{`0.32 3.14`, []syntax.Token{syntax.TNumber, syntax.TNumber}},
25+
26+
// Tags
27+
{`foo foo.bar`, []syntax.Token{syntax.TTag, syntax.TTag}},
28+
29+
// Strings (values)
30+
{` '' x 'x' 'x y'`, []syntax.Token{syntax.TString, syntax.TTag, syntax.TString, syntax.TString}},
31+
{` 'you are not your job' `, []syntax.Token{syntax.TString}},
32+
33+
// Comparison operators
34+
{`< <= = > >=`, []syntax.Token{
35+
syntax.TLt, syntax.TLeq, syntax.TEq, syntax.TGt, syntax.TGeq,
36+
}},
37+
38+
// Mixed values of various kinds.
39+
{`x AND y`, []syntax.Token{syntax.TTag, syntax.TAnd, syntax.TTag}},
40+
{`x.y CONTAINS 'z'`, []syntax.Token{syntax.TTag, syntax.TContains, syntax.TString}},
41+
{`foo EXISTS`, []syntax.Token{syntax.TTag, syntax.TExists}},
42+
{`and AND`, []syntax.Token{syntax.TTag, syntax.TAnd}},
43+
44+
// Timestamp
45+
{`TIME 2021-11-23T15:16:17Z`, []syntax.Token{syntax.TTime}},
46+
47+
// Datestamp
48+
{`DATE 2021-11-23`, []syntax.Token{syntax.TDate}},
49+
}
50+
51+
for _, test := range tests {
52+
s := syntax.NewScanner(strings.NewReader(test.input))
53+
var got []syntax.Token
54+
for s.Next() == nil {
55+
got = append(got, s.Token())
56+
}
57+
if err := s.Err(); err != io.EOF {
58+
t.Errorf("Next: unexpected error: %v", err)
59+
}
60+
61+
if !reflect.DeepEqual(got, test.want) {
62+
t.Logf("Scanner input: %q", test.input)
63+
t.Errorf("Wrong tokens:\ngot: %+v\nwant: %+v", got, test.want)
64+
}
65+
}
66+
}
67+
68+
func TestScannerErrors(t *testing.T) {
69+
tests := []struct {
70+
input string
71+
}{
72+
{`'incomplete string`},
73+
{`-23`},
74+
{`&`},
75+
{`DATE xyz-pdq`},
76+
{`DATE xyzp-dq-zv`},
77+
{`DATE 0000-00-00`},
78+
{`DATE 0000-00-000`},
79+
{`DATE 2021-01-99`},
80+
{`TIME 2021-01-01T34:56:78Z`},
81+
{`TIME 2021-01-99T14:56:08Z`},
82+
{`TIME 2021-01-99T34:56:08`},
83+
{`TIME 2021-01-99T34:56:11+3`},
84+
}
85+
for _, test := range tests {
86+
s := syntax.NewScanner(strings.NewReader(test.input))
87+
if err := s.Next(); err == nil {
88+
t.Errorf("Next: got %v (%#q), want error", s.Token(), s.Text())
89+
}
90+
}
91+
}
92+
93+
// These parser tests were copied from the original implementation of the query
94+
// parser, and are preserved here as a compatibility check.
95+
func TestParseValid(t *testing.T) {
96+
tests := []struct {
97+
input string
98+
valid bool
99+
}{
100+
{"tm.events.type='NewBlock'", true},
101+
{"tm.events.type = 'NewBlock'", true},
102+
{"tm.events.name = ''", true},
103+
{"tm.events.type='TIME'", true},
104+
{"tm.events.type='DATE'", true},
105+
{"tm.events.type='='", true},
106+
{"tm.events.type='TIME", false},
107+
{"tm.events.type=TIME'", false},
108+
{"tm.events.type==", false},
109+
{"tm.events.type=NewBlock", false},
110+
{">==", false},
111+
{"tm.events.type 'NewBlock' =", false},
112+
{"tm.events.type>'NewBlock'", false},
113+
{"", false},
114+
{"=", false},
115+
{"='NewBlock'", false},
116+
{"tm.events.type=", false},
117+
118+
{"tm.events.typeNewBlock", false},
119+
{"tm.events.type'NewBlock'", false},
120+
{"'NewBlock'", false},
121+
{"NewBlock", false},
122+
{"", false},
123+
124+
{"tm.events.type='NewBlock' AND abci.account.name='Igor'", true},
125+
{"tm.events.type='NewBlock' AND", false},
126+
{"tm.events.type='NewBlock' AN", false},
127+
{"tm.events.type='NewBlock' AN tm.events.type='NewBlockHeader'", false},
128+
{"AND tm.events.type='NewBlock' ", false},
129+
130+
{"abci.account.name CONTAINS 'Igor'", true},
131+
132+
{"tx.date > DATE 2013-05-03", true},
133+
{"tx.date < DATE 2013-05-03", true},
134+
{"tx.date <= DATE 2013-05-03", true},
135+
{"tx.date >= DATE 2013-05-03", true},
136+
{"tx.date >= DAT 2013-05-03", false},
137+
{"tx.date <= DATE2013-05-03", false},
138+
{"tx.date <= DATE -05-03", false},
139+
{"tx.date >= DATE 20130503", false},
140+
{"tx.date >= DATE 2013+01-03", false},
141+
// incorrect year, month, day
142+
{"tx.date >= DATE 0013-01-03", false},
143+
{"tx.date >= DATE 2013-31-03", false},
144+
{"tx.date >= DATE 2013-01-83", false},
145+
146+
{"tx.date > TIME 2013-05-03T14:45:00+07:00", true},
147+
{"tx.date < TIME 2013-05-03T14:45:00-02:00", true},
148+
{"tx.date <= TIME 2013-05-03T14:45:00Z", true},
149+
{"tx.date >= TIME 2013-05-03T14:45:00Z", true},
150+
{"tx.date >= TIME2013-05-03T14:45:00Z", false},
151+
{"tx.date = IME 2013-05-03T14:45:00Z", false},
152+
{"tx.date = TIME 2013-05-:45:00Z", false},
153+
{"tx.date >= TIME 2013-05-03T14:45:00", false},
154+
{"tx.date >= TIME 0013-00-00T14:45:00Z", false},
155+
{"tx.date >= TIME 2013+05=03T14:45:00Z", false},
156+
157+
{"account.balance=100", true},
158+
{"account.balance >= 200", true},
159+
{"account.balance >= -300", false},
160+
{"account.balance >>= 400", false},
161+
{"account.balance=33.22.1", false},
162+
163+
{"slashing.amount EXISTS", true},
164+
{"slashing.amount EXISTS AND account.balance=100", true},
165+
{"account.balance=100 AND slashing.amount EXISTS", true},
166+
{"slashing EXISTS", true},
167+
168+
{"hash='136E18F7E4C348B780CF873A0BF43922E5BAFA63'", true},
169+
{"hash=136E18F7E4C348B780CF873A0BF43922E5BAFA63", false},
170+
}
171+
172+
for _, test := range tests {
173+
q, err := syntax.Parse(test.input)
174+
if test.valid != (err == nil) {
175+
t.Errorf("Parse %#q: valid %v got err=%v", test.input, test.valid, err)
176+
}
177+
178+
// For valid queries, check that the query round-trips.
179+
if test.valid {
180+
qstr := q.String()
181+
r, err := syntax.Parse(qstr)
182+
if err != nil {
183+
t.Errorf("Reparse %#q failed: %v", qstr, err)
184+
}
185+
if rstr := r.String(); rstr != qstr {
186+
t.Errorf("Reparse diff\nold: %#q\nnew: %#q", qstr, rstr)
187+
}
188+
}
189+
}
190+
}

‎state/indexer/block/kv/kv.go

+10-12
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
abci "github.com/tendermint/tendermint/abci/types"
1515
"github.com/tendermint/tendermint/libs/pubsub/query"
16+
"github.com/tendermint/tendermint/libs/pubsub/query/syntax"
1617
"github.com/tendermint/tendermint/state/indexer"
1718
"github.com/tendermint/tendermint/types"
1819
)
@@ -91,10 +92,7 @@ func (idx *BlockerIndexer) Search(ctx context.Context, q *query.Query) ([]int64,
9192
default:
9293
}
9394

94-
conditions, err := q.Conditions()
95-
if err != nil {
96-
return nil, fmt.Errorf("failed to parse query conditions: %w", err)
97-
}
95+
conditions := q.Syntax()
9896

9997
// If there is an exact height query, return the result immediately
10098
// (if it exists).
@@ -158,7 +156,7 @@ func (idx *BlockerIndexer) Search(ctx context.Context, q *query.Query) ([]int64,
158156
continue
159157
}
160158

161-
startKey, err := orderedcode.Append(nil, c.CompositeKey, fmt.Sprintf("%v", c.Operand))
159+
startKey, err := orderedcode.Append(nil, c.Tag, c.Arg.Value())
162160
if err != nil {
163161
return nil, err
164162
}
@@ -326,7 +324,7 @@ LOOP:
326324
// matched.
327325
func (idx *BlockerIndexer) match(
328326
ctx context.Context,
329-
c query.Condition,
327+
c syntax.Condition,
330328
startKeyBz []byte,
331329
filteredHeights map[string][]byte,
332330
firstRun bool,
@@ -341,7 +339,7 @@ func (idx *BlockerIndexer) match(
341339
tmpHeights := make(map[string][]byte)
342340

343341
switch {
344-
case c.Op == query.OpEqual:
342+
case c.Op == syntax.TEq:
345343
it, err := dbm.IteratePrefix(idx.store, startKeyBz)
346344
if err != nil {
347345
return nil, fmt.Errorf("failed to create prefix iterator: %w", err)
@@ -360,8 +358,8 @@ func (idx *BlockerIndexer) match(
360358
return nil, err
361359
}
362360

363-
case c.Op == query.OpExists:
364-
prefix, err := orderedcode.Append(nil, c.CompositeKey)
361+
case c.Op == syntax.TExists:
362+
prefix, err := orderedcode.Append(nil, c.Tag)
365363
if err != nil {
366364
return nil, err
367365
}
@@ -387,8 +385,8 @@ func (idx *BlockerIndexer) match(
387385
return nil, err
388386
}
389387

390-
case c.Op == query.OpContains:
391-
prefix, err := orderedcode.Append(nil, c.CompositeKey)
388+
case c.Op == syntax.TContains:
389+
prefix, err := orderedcode.Append(nil, c.Tag)
392390
if err != nil {
393391
return nil, err
394392
}
@@ -405,7 +403,7 @@ func (idx *BlockerIndexer) match(
405403
continue
406404
}
407405

408-
if strings.Contains(eventValue, c.Operand.(string)) {
406+
if strings.Contains(eventValue, c.Arg.Value()) {
409407
tmpHeights[string(it.Value())] = it.Value()
410408
}
411409

‎state/indexer/block/kv/kv_test.go

+9-9
Original file line numberDiff line numberDiff line change
@@ -94,39 +94,39 @@ func TestBlockIndexer(t *testing.T) {
9494
results []int64
9595
}{
9696
"block.height = 100": {
97-
q: query.MustParse("block.height = 100"),
97+
q: query.MustCompile(`block.height = 100`),
9898
results: []int64{},
9999
},
100100
"block.height = 5": {
101-
q: query.MustParse("block.height = 5"),
101+
q: query.MustCompile(`block.height = 5`),
102102
results: []int64{5},
103103
},
104104
"begin_event.key1 = 'value1'": {
105-
q: query.MustParse("begin_event.key1 = 'value1'"),
105+
q: query.MustCompile(`begin_event.key1 = 'value1'`),
106106
results: []int64{},
107107
},
108108
"begin_event.proposer = 'FCAA001'": {
109-
q: query.MustParse("begin_event.proposer = 'FCAA001'"),
109+
q: query.MustCompile(`begin_event.proposer = 'FCAA001'`),
110110
results: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11},
111111
},
112112
"end_event.foo <= 5": {
113-
q: query.MustParse("end_event.foo <= 5"),
113+
q: query.MustCompile(`end_event.foo <= 5`),
114114
results: []int64{2, 4},
115115
},
116116
"end_event.foo >= 100": {
117-
q: query.MustParse("end_event.foo >= 100"),
117+
q: query.MustCompile(`end_event.foo >= 100`),
118118
results: []int64{1},
119119
},
120120
"block.height > 2 AND end_event.foo <= 8": {
121-
q: query.MustParse("block.height > 2 AND end_event.foo <= 8"),
121+
q: query.MustCompile(`block.height > 2 AND end_event.foo <= 8`),
122122
results: []int64{4, 6, 8},
123123
},
124124
"begin_event.proposer CONTAINS 'FFFFFFF'": {
125-
q: query.MustParse("begin_event.proposer CONTAINS 'FFFFFFF'"),
125+
q: query.MustCompile(`begin_event.proposer CONTAINS 'FFFFFFF'`),
126126
results: []int64{},
127127
},
128128
"begin_event.proposer CONTAINS 'FCAA001'": {
129-
q: query.MustParse("begin_event.proposer CONTAINS 'FCAA001'"),
129+
q: query.MustCompile(`begin_event.proposer CONTAINS 'FCAA001'`),
130130
results: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11},
131131
},
132132
}

‎state/indexer/block/kv/util.go

+4-5
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import (
66
"strconv"
77

88
"github.com/google/orderedcode"
9-
10-
"github.com/tendermint/tendermint/libs/pubsub/query"
9+
"github.com/tendermint/tendermint/libs/pubsub/query/syntax"
1110
"github.com/tendermint/tendermint/types"
1211
)
1312

@@ -86,10 +85,10 @@ func parseValueFromEventKey(key []byte) (string, error) {
8685
return eventValue, nil
8786
}
8887

89-
func lookForHeight(conditions []query.Condition) (int64, bool) {
88+
func lookForHeight(conditions []syntax.Condition) (int64, bool) {
9089
for _, c := range conditions {
91-
if c.CompositeKey == types.BlockHeightKey && c.Op == query.OpEqual {
92-
return c.Operand.(int64), true
90+
if c.Tag == types.BlockHeightKey && c.Op == syntax.TEq {
91+
return int64(c.Arg.Number()), true
9392
}
9493
}
9594

‎state/indexer/query_range.go

+29-15
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package indexer
33
import (
44
"time"
55

6-
"github.com/tendermint/tendermint/libs/pubsub/query"
6+
"github.com/tendermint/tendermint/libs/pubsub/query/syntax"
77
)
88

99
// QueryRanges defines a mapping between a composite event key and a QueryRange.
@@ -77,32 +77,32 @@ func (qr QueryRange) UpperBoundValue() interface{} {
7777

7878
// LookForRanges returns a mapping of QueryRanges and the matching indexes in
7979
// the provided query conditions.
80-
func LookForRanges(conditions []query.Condition) (ranges QueryRanges, indexes []int) {
80+
func LookForRanges(conditions []syntax.Condition) (ranges QueryRanges, indexes []int) {
8181
ranges = make(QueryRanges)
8282
for i, c := range conditions {
8383
if IsRangeOperation(c.Op) {
84-
r, ok := ranges[c.CompositeKey]
84+
r, ok := ranges[c.Tag]
8585
if !ok {
86-
r = QueryRange{Key: c.CompositeKey}
86+
r = QueryRange{Key: c.Tag}
8787
}
8888

8989
switch c.Op {
90-
case query.OpGreater:
91-
r.LowerBound = c.Operand
90+
case syntax.TGt:
91+
r.LowerBound = conditionArg(c)
9292

93-
case query.OpGreaterEqual:
93+
case syntax.TGeq:
9494
r.IncludeLowerBound = true
95-
r.LowerBound = c.Operand
95+
r.LowerBound = conditionArg(c)
9696

97-
case query.OpLess:
98-
r.UpperBound = c.Operand
97+
case syntax.TLt:
98+
r.UpperBound = conditionArg(c)
9999

100-
case query.OpLessEqual:
100+
case syntax.TLeq:
101101
r.IncludeUpperBound = true
102-
r.UpperBound = c.Operand
102+
r.UpperBound = conditionArg(c)
103103
}
104104

105-
ranges[c.CompositeKey] = r
105+
ranges[c.Tag] = r
106106
indexes = append(indexes, i)
107107
}
108108
}
@@ -112,12 +112,26 @@ func LookForRanges(conditions []query.Condition) (ranges QueryRanges, indexes []
112112

113113
// IsRangeOperation returns a boolean signifying if a query Operator is a range
114114
// operation or not.
115-
func IsRangeOperation(op query.Operator) bool {
115+
func IsRangeOperation(op syntax.Token) bool {
116116
switch op {
117-
case query.OpGreater, query.OpGreaterEqual, query.OpLess, query.OpLessEqual:
117+
case syntax.TGt, syntax.TGeq, syntax.TLt, syntax.TLeq:
118118
return true
119119

120120
default:
121121
return false
122122
}
123123
}
124+
125+
func conditionArg(c syntax.Condition) interface{} {
126+
if c.Arg == nil {
127+
return nil
128+
}
129+
switch c.Arg.Type {
130+
case syntax.TNumber:
131+
return int64(c.Arg.Number())
132+
case syntax.TTime, syntax.TDate:
133+
return c.Arg.Time()
134+
default:
135+
return c.Arg.Value() // string
136+
}
137+
}

‎state/txindex/kv/kv.go

+19-22
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
abci "github.com/tendermint/tendermint/abci/types"
1515
"github.com/tendermint/tendermint/libs/pubsub/query"
16+
"github.com/tendermint/tendermint/libs/pubsub/query/syntax"
1617
"github.com/tendermint/tendermint/state/indexer"
1718
"github.com/tendermint/tendermint/state/txindex"
1819
"github.com/tendermint/tendermint/types"
@@ -185,10 +186,7 @@ func (txi *TxIndex) Search(ctx context.Context, q *query.Query) ([]*abci.TxResul
185186
filteredHashes := make(map[string][]byte)
186187

187188
// get a list of conditions (like "tx.height > 5")
188-
conditions, err := q.Conditions()
189-
if err != nil {
190-
return nil, fmt.Errorf("error during parsing conditions from query: %w", err)
191-
}
189+
conditions := q.Syntax()
192190

193191
// if there is a hash condition, return the result immediately
194192
hash, ok, err := lookForHash(conditions)
@@ -275,21 +273,21 @@ RESULTS_LOOP:
275273
return results, nil
276274
}
277275

278-
func lookForHash(conditions []query.Condition) (hash []byte, ok bool, err error) {
276+
func lookForHash(conditions []syntax.Condition) (hash []byte, ok bool, err error) {
279277
for _, c := range conditions {
280-
if c.CompositeKey == types.TxHashKey {
281-
decoded, err := hex.DecodeString(c.Operand.(string))
278+
if c.Tag == types.TxHashKey {
279+
decoded, err := hex.DecodeString(c.Arg.Value())
282280
return decoded, true, err
283281
}
284282
}
285283
return
286284
}
287285

288286
// lookForHeight returns a height if there is an "height=X" condition.
289-
func lookForHeight(conditions []query.Condition) (height int64) {
287+
func lookForHeight(conditions []syntax.Condition) (height int64) {
290288
for _, c := range conditions {
291-
if c.CompositeKey == types.TxHeightKey && c.Op == query.OpEqual {
292-
return c.Operand.(int64)
289+
if c.Tag == types.TxHeightKey && c.Op == syntax.TEq {
290+
return int64(c.Arg.Number())
293291
}
294292
}
295293
return 0
@@ -302,7 +300,7 @@ func lookForHeight(conditions []query.Condition) (height int64) {
302300
// NOTE: filteredHashes may be empty if no previous condition has matched.
303301
func (txi *TxIndex) match(
304302
ctx context.Context,
305-
c query.Condition,
303+
c syntax.Condition,
306304
startKeyBz []byte,
307305
filteredHashes map[string][]byte,
308306
firstRun bool,
@@ -315,8 +313,8 @@ func (txi *TxIndex) match(
315313

316314
tmpHashes := make(map[string][]byte)
317315

318-
switch c.Op {
319-
case query.OpEqual:
316+
switch {
317+
case c.Op == syntax.TEq:
320318
it, err := dbm.IteratePrefix(txi.store, startKeyBz)
321319
if err != nil {
322320
panic(err)
@@ -338,10 +336,10 @@ func (txi *TxIndex) match(
338336
panic(err)
339337
}
340338

341-
case query.OpExists:
339+
case c.Op == syntax.TExists:
342340
// XXX: can't use startKeyBz here because c.Operand is nil
343341
// (e.g. "account.owner/<nil>/" won't match w/ a single row)
344-
it, err := dbm.IteratePrefix(txi.store, startKey(c.CompositeKey))
342+
it, err := dbm.IteratePrefix(txi.store, startKey(c.Tag))
345343
if err != nil {
346344
panic(err)
347345
}
@@ -362,11 +360,11 @@ func (txi *TxIndex) match(
362360
panic(err)
363361
}
364362

365-
case query.OpContains:
363+
case c.Op == syntax.TContains:
366364
// XXX: startKey does not apply here.
367365
// For example, if startKey = "account.owner/an/" and search query = "account.owner CONTAINS an"
368366
// we can't iterate with prefix "account.owner/an/" because we might miss keys like "account.owner/Ulan/"
369-
it, err := dbm.IteratePrefix(txi.store, startKey(c.CompositeKey))
367+
it, err := dbm.IteratePrefix(txi.store, startKey(c.Tag))
370368
if err != nil {
371369
panic(err)
372370
}
@@ -377,8 +375,7 @@ func (txi *TxIndex) match(
377375
if !isTagKey(it.Key()) {
378376
continue
379377
}
380-
381-
if strings.Contains(extractValueFromKey(it.Key()), c.Operand.(string)) {
378+
if strings.Contains(extractValueFromKey(it.Key()), c.Arg.Value()) {
382379
tmpHashes[string(it.Value())] = it.Value()
383380
}
384381

@@ -557,11 +554,11 @@ func keyForHeight(result *abci.TxResult) []byte {
557554
))
558555
}
559556

560-
func startKeyForCondition(c query.Condition, height int64) []byte {
557+
func startKeyForCondition(c syntax.Condition, height int64) []byte {
561558
if height > 0 {
562-
return startKey(c.CompositeKey, c.Operand, height)
559+
return startKey(c.Tag, c.Arg.Value(), height)
563560
}
564-
return startKey(c.CompositeKey, c.Operand)
561+
return startKey(c.Tag, c.Arg.Value())
565562
}
566563

567564
func startKey(fields ...interface{}) []byte {

‎state/txindex/kv/kv_bench_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ func BenchmarkTxSearch(b *testing.B) {
6060
}
6161
}
6262

63-
txQuery := query.MustParse("transfer.address = 'address_43' AND transfer.amount = 50")
63+
txQuery := query.MustCompile(`transfer.address = 'address_43' AND transfer.amount = 50`)
6464

6565
b.ResetTimer()
6666

‎state/txindex/kv/kv_test.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ func TestTxSearch(t *testing.T) {
126126
for _, tc := range testCases {
127127
tc := tc
128128
t.Run(tc.q, func(t *testing.T) {
129-
results, err := indexer.Search(ctx, query.MustParse(tc.q))
129+
results, err := indexer.Search(ctx, query.MustCompile(tc.q))
130130
assert.NoError(t, err)
131131

132132
assert.Len(t, results, tc.resultsLength)
@@ -152,7 +152,7 @@ func TestTxSearchWithCancelation(t *testing.T) {
152152

153153
ctx, cancel := context.WithCancel(context.Background())
154154
cancel()
155-
results, err := indexer.Search(ctx, query.MustParse("account.number = 1"))
155+
results, err := indexer.Search(ctx, query.MustCompile(`account.number = 1`))
156156
assert.NoError(t, err)
157157
assert.Empty(t, results)
158158
}
@@ -225,7 +225,7 @@ func TestTxSearchDeprecatedIndexing(t *testing.T) {
225225
for _, tc := range testCases {
226226
tc := tc
227227
t.Run(tc.q, func(t *testing.T) {
228-
results, err := indexer.Search(ctx, query.MustParse(tc.q))
228+
results, err := indexer.Search(ctx, query.MustCompile(tc.q))
229229
require.NoError(t, err)
230230
for _, txr := range results {
231231
for _, tr := range tc.results {
@@ -249,7 +249,7 @@ func TestTxSearchOneTxWithMultipleSameTagsButDifferentValues(t *testing.T) {
249249

250250
ctx := context.Background()
251251

252-
results, err := indexer.Search(ctx, query.MustParse("account.number >= 1"))
252+
results, err := indexer.Search(ctx, query.MustCompile(`account.number >= 1`))
253253
assert.NoError(t, err)
254254

255255
assert.Len(t, results, 1)
@@ -306,7 +306,7 @@ func TestTxSearchMultipleTxs(t *testing.T) {
306306

307307
ctx := context.Background()
308308

309-
results, err := indexer.Search(ctx, query.MustParse("account.number >= 1"))
309+
results, err := indexer.Search(ctx, query.MustCompile(`account.number >= 1`))
310310
assert.NoError(t, err)
311311

312312
require.Len(t, results, 3)

‎types/event_bus_test.go

+6-6
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func TestEventBusPublishEventTx(t *testing.T) {
3636

3737
// PublishEventTx adds 3 composite keys, so the query below should work
3838
query := fmt.Sprintf("tm.event='Tx' AND tx.height=1 AND tx.hash='%X' AND testType.baz=1", tx.Hash())
39-
txsSub, err := eventBus.Subscribe(context.Background(), "test", tmquery.MustParse(query))
39+
txsSub, err := eventBus.Subscribe(context.Background(), "test", tmquery.MustCompile(query))
4040
require.NoError(t, err)
4141

4242
done := make(chan struct{})
@@ -89,7 +89,7 @@ func TestEventBusPublishEventNewBlock(t *testing.T) {
8989

9090
// PublishEventNewBlock adds the tm.event compositeKey, so the query below should work
9191
query := "tm.event='NewBlock' AND testType.baz=1 AND testType.foz=2"
92-
blocksSub, err := eventBus.Subscribe(context.Background(), "test", tmquery.MustParse(query))
92+
blocksSub, err := eventBus.Subscribe(context.Background(), "test", tmquery.MustCompile(query))
9393
require.NoError(t, err)
9494

9595
done := make(chan struct{})
@@ -184,7 +184,7 @@ func TestEventBusPublishEventTxDuplicateKeys(t *testing.T) {
184184
}
185185

186186
for i, tc := range testCases {
187-
sub, err := eventBus.Subscribe(context.Background(), fmt.Sprintf("client-%d", i), tmquery.MustParse(tc.query))
187+
sub, err := eventBus.Subscribe(context.Background(), fmt.Sprintf("client-%d", i), tmquery.MustCompile(tc.query))
188188
require.NoError(t, err)
189189

190190
done := make(chan struct{})
@@ -248,7 +248,7 @@ func TestEventBusPublishEventNewBlockHeader(t *testing.T) {
248248

249249
// PublishEventNewBlockHeader adds the tm.event compositeKey, so the query below should work
250250
query := "tm.event='NewBlockHeader' AND testType.baz=1 AND testType.foz=2"
251-
headersSub, err := eventBus.Subscribe(context.Background(), "test", tmquery.MustParse(query))
251+
headersSub, err := eventBus.Subscribe(context.Background(), "test", tmquery.MustCompile(query))
252252
require.NoError(t, err)
253253

254254
done := make(chan struct{})
@@ -289,7 +289,7 @@ func TestEventBusPublishEventNewEvidence(t *testing.T) {
289289
require.NoError(t, err)
290290

291291
query := "tm.event='NewEvidence'"
292-
evSub, err := eventBus.Subscribe(context.Background(), "test", tmquery.MustParse(query))
292+
evSub, err := eventBus.Subscribe(context.Background(), "test", tmquery.MustCompile(query))
293293
require.NoError(t, err)
294294

295295
done := make(chan struct{})
@@ -326,7 +326,7 @@ func TestEventBusPublish(t *testing.T) {
326326

327327
const numEventsExpected = 14
328328

329-
sub, err := eventBus.Subscribe(context.Background(), "test", tmquery.Empty{}, numEventsExpected)
329+
sub, err := eventBus.Subscribe(context.Background(), "test", tmquery.All, numEventsExpected)
330330
require.NoError(t, err)
331331

332332
done := make(chan struct{})

‎types/events.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -162,11 +162,11 @@ var (
162162
)
163163

164164
func EventQueryTxFor(tx Tx) tmpubsub.Query {
165-
return tmquery.MustParse(fmt.Sprintf("%s='%s' AND %s='%X'", EventTypeKey, EventTx, TxHashKey, tx.Hash()))
165+
return tmquery.MustCompile(fmt.Sprintf("%s='%s' AND %s='%X'", EventTypeKey, EventTx, TxHashKey, tx.Hash()))
166166
}
167167

168168
func QueryForEvent(eventType string) tmpubsub.Query {
169-
return tmquery.MustParse(fmt.Sprintf("%s='%s'", EventTypeKey, eventType))
169+
return tmquery.MustCompile(fmt.Sprintf("%s='%s'", EventTypeKey, eventType))
170170
}
171171

172172
// BlockEventPublisher publishes all block related events

‎types/events_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,18 @@ import (
1010
func TestQueryTxFor(t *testing.T) {
1111
tx := Tx("foo")
1212
assert.Equal(t,
13-
fmt.Sprintf("tm.event='Tx' AND tx.hash='%X'", tx.Hash()),
13+
fmt.Sprintf("tm.event = 'Tx' AND tx.hash = '%X'", tx.Hash()),
1414
EventQueryTxFor(tx).String(),
1515
)
1616
}
1717

1818
func TestQueryForEvent(t *testing.T) {
1919
assert.Equal(t,
20-
"tm.event='NewBlock'",
20+
"tm.event = 'NewBlock'",
2121
QueryForEvent(EventNewBlock).String(),
2222
)
2323
assert.Equal(t,
24-
"tm.event='NewEvidence'",
24+
"tm.event = 'NewEvidence'",
2525
QueryForEvent(EventNewEvidence).String(),
2626
)
2727
}

0 commit comments

Comments
 (0)
Please sign in to comment.