mirror of
https://github.com/tendermint/tendermint.git
synced 2026-01-05 13:05:09 +00:00
@@ -4,7 +4,6 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/tendermint/tendermint/libs/pubsub/query"
|
"github.com/tendermint/tendermint/libs/pubsub/query"
|
||||||
oldquery "github.com/tendermint/tendermint/libs/pubsub/query/oldquery"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const testQuery = `tm.events.type='NewBlock' AND abci.account.name='Igor'`
|
const testQuery = `tm.events.type='NewBlock' AND abci.account.name='Igor'`
|
||||||
@@ -21,15 +20,6 @@ var testEvents = map[string][]string{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkParsePEG(b *testing.B) {
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
_, err := oldquery.New(testQuery)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkParseCustom(b *testing.B) {
|
func BenchmarkParseCustom(b *testing.B) {
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
_, err := query.New(testQuery)
|
_, err := query.New(testQuery)
|
||||||
@@ -39,22 +29,6 @@ func BenchmarkParseCustom(b *testing.B) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func BenchmarkMatchPEG(b *testing.B) {
|
|
||||||
q, err := oldquery.New(testQuery)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
}
|
|
||||||
b.ResetTimer()
|
|
||||||
for i := 0; i < b.N; i++ {
|
|
||||||
ok, err := q.Matches(testEvents)
|
|
||||||
if err != nil {
|
|
||||||
b.Fatal(err)
|
|
||||||
} else if !ok {
|
|
||||||
b.Error("no match")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BenchmarkMatchCustom(b *testing.B) {
|
func BenchmarkMatchCustom(b *testing.B) {
|
||||||
q, err := query.New(testQuery)
|
q, err := query.New(testQuery)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
gen_query_parser:
|
|
||||||
go generate .
|
|
||||||
|
|
||||||
fuzzy_test:
|
|
||||||
go get -u -v github.com/dvyukov/go-fuzz/go-fuzz
|
|
||||||
go get -u -v github.com/dvyukov/go-fuzz/go-fuzz-build
|
|
||||||
go-fuzz-build github.com/tendermint/tendermint/libs/pubsub/query/fuzz_test
|
|
||||||
go-fuzz -bin=./fuzz_test-fuzz.zip -workdir=./fuzz_test/output
|
|
||||||
|
|
||||||
.PHONY: fuzzy_test
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
package query
|
|
||||||
|
|
||||||
// Empty query matches any set of events.
|
|
||||||
type Empty struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
// Matches always returns true.
|
|
||||||
func (Empty) Matches(tags map[string][]string) (bool, error) {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (Empty) String() string {
|
|
||||||
return "empty"
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
package query_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
query "github.com/tendermint/tendermint/libs/pubsub/query/oldquery"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestEmptyQueryMatchesAnything(t *testing.T) {
|
|
||||||
q := query.Empty{}
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
query map[string][]string
|
|
||||||
}{
|
|
||||||
{map[string][]string{}},
|
|
||||||
{map[string][]string{"Asher": {"Roth"}}},
|
|
||||||
{map[string][]string{"Route": {"66"}}},
|
|
||||||
{map[string][]string{"Route": {"66"}, "Billy": {"Blue"}}},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
match, err := q.Matches(tc.query)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.True(t, match)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package fuzz_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
query "github.com/tendermint/tendermint/libs/pubsub/query/oldquery"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Fuzz(data []byte) int {
|
|
||||||
sdata := string(data)
|
|
||||||
q0, err := query.New(sdata)
|
|
||||||
if err != nil {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
sdata1 := q0.String()
|
|
||||||
q1, err := query.New(sdata1)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sdata2 := q1.String()
|
|
||||||
if sdata1 != sdata2 {
|
|
||||||
fmt.Printf("q0: %q\n", sdata1)
|
|
||||||
fmt.Printf("q1: %q\n", sdata2)
|
|
||||||
panic("query changed")
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
package query_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
|
|
||||||
query "github.com/tendermint/tendermint/libs/pubsub/query/oldquery"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TODO: fuzzy testing?
|
|
||||||
func TestParser(t *testing.T) {
|
|
||||||
cases := []struct {
|
|
||||||
query string
|
|
||||||
valid bool
|
|
||||||
}{
|
|
||||||
{"tm.events.type='NewBlock'", true},
|
|
||||||
{"tm.events.type = 'NewBlock'", true},
|
|
||||||
{"tm.events.name = ''", true},
|
|
||||||
{"tm.events.type='TIME'", true},
|
|
||||||
{"tm.events.type='DATE'", true},
|
|
||||||
{"tm.events.type='='", true},
|
|
||||||
{"tm.events.type='TIME", false},
|
|
||||||
{"tm.events.type=TIME'", false},
|
|
||||||
{"tm.events.type==", false},
|
|
||||||
{"tm.events.type=NewBlock", false},
|
|
||||||
{">==", false},
|
|
||||||
{"tm.events.type 'NewBlock' =", false},
|
|
||||||
{"tm.events.type>'NewBlock'", false},
|
|
||||||
{"", false},
|
|
||||||
{"=", false},
|
|
||||||
{"='NewBlock'", false},
|
|
||||||
{"tm.events.type=", false},
|
|
||||||
|
|
||||||
{"tm.events.typeNewBlock", false},
|
|
||||||
{"tm.events.type'NewBlock'", false},
|
|
||||||
{"'NewBlock'", false},
|
|
||||||
{"NewBlock", false},
|
|
||||||
{"", false},
|
|
||||||
|
|
||||||
{"tm.events.type='NewBlock' AND abci.account.name='Igor'", true},
|
|
||||||
{"tm.events.type='NewBlock' AND", false},
|
|
||||||
{"tm.events.type='NewBlock' AN", false},
|
|
||||||
{"tm.events.type='NewBlock' AN tm.events.type='NewBlockHeader'", false},
|
|
||||||
{"AND tm.events.type='NewBlock' ", false},
|
|
||||||
|
|
||||||
{"abci.account.name CONTAINS 'Igor'", true},
|
|
||||||
|
|
||||||
{"tx.date > DATE 2013-05-03", true},
|
|
||||||
{"tx.date < DATE 2013-05-03", true},
|
|
||||||
{"tx.date <= DATE 2013-05-03", true},
|
|
||||||
{"tx.date >= DATE 2013-05-03", true},
|
|
||||||
{"tx.date >= DAT 2013-05-03", false},
|
|
||||||
{"tx.date <= DATE2013-05-03", false},
|
|
||||||
{"tx.date <= DATE -05-03", false},
|
|
||||||
{"tx.date >= DATE 20130503", false},
|
|
||||||
{"tx.date >= DATE 2013+01-03", false},
|
|
||||||
// incorrect year, month, day
|
|
||||||
{"tx.date >= DATE 0013-01-03", false},
|
|
||||||
{"tx.date >= DATE 2013-31-03", false},
|
|
||||||
{"tx.date >= DATE 2013-01-83", false},
|
|
||||||
|
|
||||||
{"tx.date > TIME 2013-05-03T14:45:00+07:00", true},
|
|
||||||
{"tx.date < TIME 2013-05-03T14:45:00-02:00", true},
|
|
||||||
{"tx.date <= TIME 2013-05-03T14:45:00Z", true},
|
|
||||||
{"tx.date >= TIME 2013-05-03T14:45:00Z", true},
|
|
||||||
{"tx.date >= TIME2013-05-03T14:45:00Z", false},
|
|
||||||
{"tx.date = IME 2013-05-03T14:45:00Z", false},
|
|
||||||
{"tx.date = TIME 2013-05-:45:00Z", false},
|
|
||||||
{"tx.date >= TIME 2013-05-03T14:45:00", false},
|
|
||||||
{"tx.date >= TIME 0013-00-00T14:45:00Z", false},
|
|
||||||
{"tx.date >= TIME 2013+05=03T14:45:00Z", false},
|
|
||||||
|
|
||||||
{"account.balance=100", true},
|
|
||||||
{"account.balance >= 200", true},
|
|
||||||
{"account.balance >= -300", false},
|
|
||||||
{"account.balance >>= 400", false},
|
|
||||||
{"account.balance=33.22.1", false},
|
|
||||||
|
|
||||||
{"slashing.amount EXISTS", true},
|
|
||||||
{"slashing.amount EXISTS AND account.balance=100", true},
|
|
||||||
{"account.balance=100 AND slashing.amount EXISTS", true},
|
|
||||||
{"slashing EXISTS", true},
|
|
||||||
|
|
||||||
{"hash='136E18F7E4C348B780CF873A0BF43922E5BAFA63'", true},
|
|
||||||
{"hash=136E18F7E4C348B780CF873A0BF43922E5BAFA63", false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, c := range cases {
|
|
||||||
_, err := query.New(c.query)
|
|
||||||
if c.valid {
|
|
||||||
assert.NoErrorf(t, err, "Query was '%s'", c.query)
|
|
||||||
} else {
|
|
||||||
assert.Errorf(t, err, "Query was '%s'", c.query)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
package query
|
|
||||||
|
|
||||||
//go:generate go run github.com/pointlander/peg@v1.0.0 -inline -switch query.peg
|
|
||||||
@@ -1,504 +0,0 @@
|
|||||||
// Package query provides a parser for a custom query format:
|
|
||||||
//
|
|
||||||
// abci.invoice.number=22 AND abci.invoice.owner=Ivan
|
|
||||||
//
|
|
||||||
// See query.peg for the grammar, which is a https://en.wikipedia.org/wiki/Parsing_expression_grammar.
|
|
||||||
// More: https://github.com/PhilippeSigaud/Pegged/wiki/PEG-Basics
|
|
||||||
//
|
|
||||||
// It has a support for numbers (integer and floating point), dates and times.
|
|
||||||
package query
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
numRegex = regexp.MustCompile(`([0-9\.]+)`)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Query holds the query string and the query parser.
|
|
||||||
type Query struct {
|
|
||||||
str string
|
|
||||||
parser *QueryParser
|
|
||||||
}
|
|
||||||
|
|
||||||
// Condition represents a single condition within a query and consists of composite key
|
|
||||||
// (e.g. "tx.gas"), operator (e.g. "=") and operand (e.g. "7").
|
|
||||||
type Condition struct {
|
|
||||||
CompositeKey string
|
|
||||||
Op Operator
|
|
||||||
Operand interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// New parses the given string and returns a query or error if the string is
|
|
||||||
// invalid.
|
|
||||||
func New(s string) (*Query, error) {
|
|
||||||
p := &QueryParser{Buffer: fmt.Sprintf(`"%s"`, s)}
|
|
||||||
if err := p.Init(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := p.Parse(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &Query{str: s, parser: p}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustParse turns the given string into a query or panics; for tests or others
|
|
||||||
// cases where you know the string is valid.
|
|
||||||
func MustParse(s string) *Query {
|
|
||||||
q, err := New(s)
|
|
||||||
if err != nil {
|
|
||||||
panic(fmt.Sprintf("failed to parse %s: %v", s, err))
|
|
||||||
}
|
|
||||||
return q
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the original string.
|
|
||||||
func (q *Query) String() string {
|
|
||||||
return q.str
|
|
||||||
}
|
|
||||||
|
|
||||||
// Operator is an operator that defines some kind of relation between composite key and
|
|
||||||
// operand (equality, etc.).
|
|
||||||
type Operator uint8
|
|
||||||
|
|
||||||
const (
|
|
||||||
// "<="
|
|
||||||
OpLessEqual Operator = iota
|
|
||||||
// ">="
|
|
||||||
OpGreaterEqual
|
|
||||||
// "<"
|
|
||||||
OpLess
|
|
||||||
// ">"
|
|
||||||
OpGreater
|
|
||||||
// "="
|
|
||||||
OpEqual
|
|
||||||
// "CONTAINS"; used to check if a string contains a certain sub string.
|
|
||||||
OpContains
|
|
||||||
// "EXISTS"; used to check if a certain event attribute is present.
|
|
||||||
OpExists
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// DateLayout defines a layout for all dates (`DATE date`)
|
|
||||||
DateLayout = "2006-01-02"
|
|
||||||
// TimeLayout defines a layout for all times (`TIME time`)
|
|
||||||
TimeLayout = time.RFC3339
|
|
||||||
)
|
|
||||||
|
|
||||||
// Conditions returns a list of conditions. It returns an error if there is any
|
|
||||||
// error with the provided grammar in the Query.
|
|
||||||
func (q *Query) Conditions() ([]Condition, error) {
|
|
||||||
var (
|
|
||||||
eventAttr string
|
|
||||||
op Operator
|
|
||||||
)
|
|
||||||
|
|
||||||
conditions := make([]Condition, 0)
|
|
||||||
buffer, begin, end := q.parser.Buffer, 0, 0
|
|
||||||
|
|
||||||
// tokens must be in the following order: tag ("tx.gas") -> operator ("=") -> operand ("7")
|
|
||||||
for _, token := range q.parser.Tokens() {
|
|
||||||
switch token.pegRule {
|
|
||||||
case rulePegText:
|
|
||||||
begin, end = int(token.begin), int(token.end)
|
|
||||||
|
|
||||||
case ruletag:
|
|
||||||
eventAttr = buffer[begin:end]
|
|
||||||
|
|
||||||
case rulele:
|
|
||||||
op = OpLessEqual
|
|
||||||
|
|
||||||
case rulege:
|
|
||||||
op = OpGreaterEqual
|
|
||||||
|
|
||||||
case rulel:
|
|
||||||
op = OpLess
|
|
||||||
|
|
||||||
case ruleg:
|
|
||||||
op = OpGreater
|
|
||||||
|
|
||||||
case ruleequal:
|
|
||||||
op = OpEqual
|
|
||||||
|
|
||||||
case rulecontains:
|
|
||||||
op = OpContains
|
|
||||||
|
|
||||||
case ruleexists:
|
|
||||||
op = OpExists
|
|
||||||
conditions = append(conditions, Condition{eventAttr, op, nil})
|
|
||||||
|
|
||||||
case rulevalue:
|
|
||||||
// strip single quotes from value (i.e. "'NewBlock'" -> "NewBlock")
|
|
||||||
valueWithoutSingleQuotes := buffer[begin+1 : end-1]
|
|
||||||
conditions = append(conditions, Condition{eventAttr, op, valueWithoutSingleQuotes})
|
|
||||||
|
|
||||||
case rulenumber:
|
|
||||||
number := buffer[begin:end]
|
|
||||||
if strings.ContainsAny(number, ".") { // if it looks like a floating-point number
|
|
||||||
value, err := strconv.ParseFloat(number, 64)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf(
|
|
||||||
"got %v while trying to parse %s as float64 (should never happen if the grammar is correct)",
|
|
||||||
err, number,
|
|
||||||
)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
conditions = append(conditions, Condition{eventAttr, op, value})
|
|
||||||
} else {
|
|
||||||
value, err := strconv.ParseInt(number, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf(
|
|
||||||
"got %v while trying to parse %s as int64 (should never happen if the grammar is correct)",
|
|
||||||
err, number,
|
|
||||||
)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
conditions = append(conditions, Condition{eventAttr, op, value})
|
|
||||||
}
|
|
||||||
|
|
||||||
case ruletime:
|
|
||||||
value, err := time.Parse(TimeLayout, buffer[begin:end])
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf(
|
|
||||||
"got %v while trying to parse %s as time.Time / RFC3339 (should never happen if the grammar is correct)",
|
|
||||||
err, buffer[begin:end],
|
|
||||||
)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
conditions = append(conditions, Condition{eventAttr, op, value})
|
|
||||||
|
|
||||||
case ruledate:
|
|
||||||
value, err := time.Parse("2006-01-02", buffer[begin:end])
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf(
|
|
||||||
"got %v while trying to parse %s as time.Time / '2006-01-02' (should never happen if the grammar is correct)",
|
|
||||||
err, buffer[begin:end],
|
|
||||||
)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
conditions = append(conditions, Condition{eventAttr, op, value})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return conditions, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Matches returns true if the query matches against any event in the given set
|
|
||||||
// of events, false otherwise. For each event, a match exists if the query is
|
|
||||||
// matched against *any* value in a slice of values. An error is returned if
|
|
||||||
// any attempted event match returns an error.
|
|
||||||
//
|
|
||||||
// For example, query "name=John" matches events = {"name": ["John", "Eric"]}.
|
|
||||||
// More examples could be found in parser_test.go and query_test.go.
|
|
||||||
func (q *Query) Matches(events map[string][]string) (bool, error) {
|
|
||||||
if len(events) == 0 {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
eventAttr string
|
|
||||||
op Operator
|
|
||||||
)
|
|
||||||
|
|
||||||
buffer, begin, end := q.parser.Buffer, 0, 0
|
|
||||||
|
|
||||||
// tokens must be in the following order:
|
|
||||||
|
|
||||||
// tag ("tx.gas") -> operator ("=") -> operand ("7")
|
|
||||||
for _, token := range q.parser.Tokens() {
|
|
||||||
switch token.pegRule {
|
|
||||||
case rulePegText:
|
|
||||||
begin, end = int(token.begin), int(token.end)
|
|
||||||
|
|
||||||
case ruletag:
|
|
||||||
eventAttr = buffer[begin:end]
|
|
||||||
|
|
||||||
case rulele:
|
|
||||||
op = OpLessEqual
|
|
||||||
|
|
||||||
case rulege:
|
|
||||||
op = OpGreaterEqual
|
|
||||||
|
|
||||||
case rulel:
|
|
||||||
op = OpLess
|
|
||||||
|
|
||||||
case ruleg:
|
|
||||||
op = OpGreater
|
|
||||||
|
|
||||||
case ruleequal:
|
|
||||||
op = OpEqual
|
|
||||||
|
|
||||||
case rulecontains:
|
|
||||||
op = OpContains
|
|
||||||
case ruleexists:
|
|
||||||
op = OpExists
|
|
||||||
if strings.Contains(eventAttr, ".") {
|
|
||||||
// Searching for a full "type.attribute" event.
|
|
||||||
_, ok := events[eventAttr]
|
|
||||||
if !ok {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
foundEvent := false
|
|
||||||
|
|
||||||
loop:
|
|
||||||
for compositeKey := range events {
|
|
||||||
if strings.Index(compositeKey, eventAttr) == 0 {
|
|
||||||
foundEvent = true
|
|
||||||
break loop
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !foundEvent {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case rulevalue:
|
|
||||||
// strip single quotes from value (i.e. "'NewBlock'" -> "NewBlock")
|
|
||||||
valueWithoutSingleQuotes := buffer[begin+1 : end-1]
|
|
||||||
|
|
||||||
// see if the triplet (event attribute, operator, operand) matches any event
|
|
||||||
// "tx.gas", "=", "7", { "tx.gas": 7, "tx.ID": "4AE393495334" }
|
|
||||||
match, err := match(eventAttr, op, reflect.ValueOf(valueWithoutSingleQuotes), events)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !match {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
case rulenumber:
|
|
||||||
number := buffer[begin:end]
|
|
||||||
if strings.ContainsAny(number, ".") { // if it looks like a floating-point number
|
|
||||||
value, err := strconv.ParseFloat(number, 64)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf(
|
|
||||||
"got %v while trying to parse %s as float64 (should never happen if the grammar is correct)",
|
|
||||||
err, number,
|
|
||||||
)
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
match, err := match(eventAttr, op, reflect.ValueOf(value), events)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !match {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
value, err := strconv.ParseInt(number, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf(
|
|
||||||
"got %v while trying to parse %s as int64 (should never happen if the grammar is correct)",
|
|
||||||
err, number,
|
|
||||||
)
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
match, err := match(eventAttr, op, reflect.ValueOf(value), events)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !match {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case ruletime:
|
|
||||||
value, err := time.Parse(TimeLayout, buffer[begin:end])
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf(
|
|
||||||
"got %v while trying to parse %s as time.Time / RFC3339 (should never happen if the grammar is correct)",
|
|
||||||
err, buffer[begin:end],
|
|
||||||
)
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
match, err := match(eventAttr, op, reflect.ValueOf(value), events)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !match {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
case ruledate:
|
|
||||||
value, err := time.Parse("2006-01-02", buffer[begin:end])
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf(
|
|
||||||
"got %v while trying to parse %s as time.Time / '2006-01-02' (should never happen if the grammar is correct)",
|
|
||||||
err, buffer[begin:end],
|
|
||||||
)
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
match, err := match(eventAttr, op, reflect.ValueOf(value), events)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !match {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// match returns true if the given triplet (attribute, operator, operand) matches
|
|
||||||
// any value in an event for that attribute. If any match fails with an error,
|
|
||||||
// that error is returned.
|
|
||||||
//
|
|
||||||
// First, it looks up the key in the events and if it finds one, tries to compare
|
|
||||||
// all the values from it to the operand using the operator.
|
|
||||||
//
|
|
||||||
// "tx.gas", "=", "7", {"tx": [{"gas": 7, "ID": "4AE393495334"}]}
|
|
||||||
func match(attr string, op Operator, operand reflect.Value, events map[string][]string) (bool, error) {
|
|
||||||
// look up the tag from the query in tags
|
|
||||||
values, ok := events[attr]
|
|
||||||
if !ok {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, value := range values {
|
|
||||||
// return true if any value in the set of the event's values matches
|
|
||||||
match, err := matchValue(value, op, operand)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if match {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// matchValue will attempt to match a string value against an operator an
|
|
||||||
// operand. A boolean is returned representing the match result. It will return
|
|
||||||
// an error if the value cannot be parsed and matched against the operand type.
|
|
||||||
func matchValue(value string, op Operator, operand reflect.Value) (bool, error) {
|
|
||||||
switch operand.Kind() {
|
|
||||||
case reflect.Struct: // time
|
|
||||||
operandAsTime := operand.Interface().(time.Time)
|
|
||||||
|
|
||||||
// try our best to convert value from events to time.Time
|
|
||||||
var (
|
|
||||||
v time.Time
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
if strings.ContainsAny(value, "T") {
|
|
||||||
v, err = time.Parse(TimeLayout, value)
|
|
||||||
} else {
|
|
||||||
v, err = time.Parse(DateLayout, value)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("failed to convert value %v from event attribute to time.Time: %w", value, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch op {
|
|
||||||
case OpLessEqual:
|
|
||||||
return (v.Before(operandAsTime) || v.Equal(operandAsTime)), nil
|
|
||||||
case OpGreaterEqual:
|
|
||||||
return (v.Equal(operandAsTime) || v.After(operandAsTime)), nil
|
|
||||||
case OpLess:
|
|
||||||
return v.Before(operandAsTime), nil
|
|
||||||
case OpGreater:
|
|
||||||
return v.After(operandAsTime), nil
|
|
||||||
case OpEqual:
|
|
||||||
return v.Equal(operandAsTime), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
case reflect.Float64:
|
|
||||||
var v float64
|
|
||||||
|
|
||||||
operandFloat64 := operand.Interface().(float64)
|
|
||||||
filteredValue := numRegex.FindString(value)
|
|
||||||
|
|
||||||
// try our best to convert value from tags to float64
|
|
||||||
v, err := strconv.ParseFloat(filteredValue, 64)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("failed to convert value %v from event attribute to float64: %w", filteredValue, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch op {
|
|
||||||
case OpLessEqual:
|
|
||||||
return v <= operandFloat64, nil
|
|
||||||
case OpGreaterEqual:
|
|
||||||
return v >= operandFloat64, nil
|
|
||||||
case OpLess:
|
|
||||||
return v < operandFloat64, nil
|
|
||||||
case OpGreater:
|
|
||||||
return v > operandFloat64, nil
|
|
||||||
case OpEqual:
|
|
||||||
return v == operandFloat64, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
case reflect.Int64:
|
|
||||||
var v int64
|
|
||||||
|
|
||||||
operandInt := operand.Interface().(int64)
|
|
||||||
filteredValue := numRegex.FindString(value)
|
|
||||||
|
|
||||||
// if value looks like float, we try to parse it as float
|
|
||||||
if strings.ContainsAny(filteredValue, ".") {
|
|
||||||
v1, err := strconv.ParseFloat(filteredValue, 64)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("failed to convert value %v from event attribute to float64: %w", filteredValue, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
v = int64(v1)
|
|
||||||
} else {
|
|
||||||
var err error
|
|
||||||
// try our best to convert value from tags to int64
|
|
||||||
v, err = strconv.ParseInt(filteredValue, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return false, fmt.Errorf("failed to convert value %v from event attribute to int64: %w", filteredValue, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch op {
|
|
||||||
case OpLessEqual:
|
|
||||||
return v <= operandInt, nil
|
|
||||||
case OpGreaterEqual:
|
|
||||||
return v >= operandInt, nil
|
|
||||||
case OpLess:
|
|
||||||
return v < operandInt, nil
|
|
||||||
case OpGreater:
|
|
||||||
return v > operandInt, nil
|
|
||||||
case OpEqual:
|
|
||||||
return v == operandInt, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
case reflect.String:
|
|
||||||
switch op {
|
|
||||||
case OpEqual:
|
|
||||||
return value == operand.String(), nil
|
|
||||||
case OpContains:
|
|
||||||
return strings.Contains(value, operand.String()), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
return false, fmt.Errorf("unknown kind of operand %v", operand.Kind())
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
package query
|
|
||||||
|
|
||||||
type QueryParser Peg {
|
|
||||||
}
|
|
||||||
|
|
||||||
e <- '\"' condition ( ' '+ and ' '+ condition )* '\"' !.
|
|
||||||
|
|
||||||
condition <- tag ' '* (le ' '* (number / time / date)
|
|
||||||
/ ge ' '* (number / time / date)
|
|
||||||
/ l ' '* (number / time / date)
|
|
||||||
/ g ' '* (number / time / date)
|
|
||||||
/ equal ' '* (number / time / date / value)
|
|
||||||
/ contains ' '* value
|
|
||||||
/ exists
|
|
||||||
)
|
|
||||||
|
|
||||||
tag <- < (![ \t\n\r\\()"'=><] .)+ >
|
|
||||||
value <- < '\'' (!["'] .)* '\''>
|
|
||||||
number <- < ('0'
|
|
||||||
/ [1-9] digit* ('.' digit*)?) >
|
|
||||||
digit <- [0-9]
|
|
||||||
time <- "TIME " < year '-' month '-' day 'T' digit digit ':' digit digit ':' digit digit (('-' / '+') digit digit ':' digit digit / 'Z') >
|
|
||||||
date <- "DATE " < year '-' month '-' day >
|
|
||||||
year <- ('1' / '2') digit digit digit
|
|
||||||
month <- ('0' / '1') digit
|
|
||||||
day <- ('0' / '1' / '2' / '3') digit
|
|
||||||
and <- "AND"
|
|
||||||
|
|
||||||
equal <- "="
|
|
||||||
contains <- "CONTAINS"
|
|
||||||
exists <- "EXISTS"
|
|
||||||
le <- "<="
|
|
||||||
ge <- ">="
|
|
||||||
l <- "<"
|
|
||||||
g <- ">"
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,180 +0,0 @@
|
|||||||
package query_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
query "github.com/tendermint/tendermint/libs/pubsub/query/oldquery"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMatches(t *testing.T) {
|
|
||||||
var (
|
|
||||||
txDate = "2017-01-01"
|
|
||||||
txTime = "2018-05-03T14:45:00Z"
|
|
||||||
)
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
s string
|
|
||||||
events map[string][]string
|
|
||||||
matches bool
|
|
||||||
}{
|
|
||||||
{"tm.events.type='NewBlock'", map[string][]string{"tm.events.type": {"NewBlock"}}, true},
|
|
||||||
{"tx.gas > 7", map[string][]string{"tx.gas": {"8"}}, true},
|
|
||||||
{"transfer.amount > 7", map[string][]string{"transfer.amount": {"8stake"}}, true},
|
|
||||||
{"transfer.amount > 7", map[string][]string{"transfer.amount": {"8.045stake"}}, true},
|
|
||||||
{"transfer.amount > 7.043", map[string][]string{"transfer.amount": {"8.045stake"}}, true},
|
|
||||||
{"transfer.amount > 8.045", map[string][]string{"transfer.amount": {"8.045stake"}}, false},
|
|
||||||
{"tx.gas > 7 AND tx.gas < 9", map[string][]string{"tx.gas": {"8"}}, true},
|
|
||||||
{"body.weight >= 3.5", map[string][]string{"body.weight": {"3.5"}}, true},
|
|
||||||
{"account.balance < 1000.0", map[string][]string{"account.balance": {"900"}}, true},
|
|
||||||
{"apples.kg <= 4", map[string][]string{"apples.kg": {"4.0"}}, true},
|
|
||||||
{"body.weight >= 4.5", map[string][]string{"body.weight": {fmt.Sprintf("%v", float32(4.5))}}, true},
|
|
||||||
{
|
|
||||||
"oranges.kg < 4 AND watermellons.kg > 10",
|
|
||||||
map[string][]string{"oranges.kg": {"3"}, "watermellons.kg": {"12"}},
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{"peaches.kg < 4", map[string][]string{"peaches.kg": {"5"}}, false},
|
|
||||||
{
|
|
||||||
"tx.date > DATE 2017-01-01",
|
|
||||||
map[string][]string{"tx.date": {time.Now().Format(query.DateLayout)}},
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{"tx.date = DATE 2017-01-01", map[string][]string{"tx.date": {txDate}}, true},
|
|
||||||
{"tx.date = DATE 2018-01-01", map[string][]string{"tx.date": {txDate}}, false},
|
|
||||||
{
|
|
||||||
"tx.time >= TIME 2013-05-03T14:45:00Z",
|
|
||||||
map[string][]string{"tx.time": {time.Now().Format(query.TimeLayout)}},
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{"tx.time = TIME 2013-05-03T14:45:00Z", map[string][]string{"tx.time": {txTime}}, false},
|
|
||||||
{"abci.owner.name CONTAINS 'Igor'", map[string][]string{"abci.owner.name": {"Igor,Ivan"}}, true},
|
|
||||||
{"abci.owner.name CONTAINS 'Igor'", map[string][]string{"abci.owner.name": {"Pavel,Ivan"}}, false},
|
|
||||||
{"abci.owner.name = 'Igor'", map[string][]string{"abci.owner.name": {"Igor", "Ivan"}}, true},
|
|
||||||
{
|
|
||||||
"abci.owner.name = 'Ivan'",
|
|
||||||
map[string][]string{"abci.owner.name": {"Igor", "Ivan"}},
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"abci.owner.name = 'Ivan' AND abci.owner.name = 'Igor'",
|
|
||||||
map[string][]string{"abci.owner.name": {"Igor", "Ivan"}},
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"abci.owner.name = 'Ivan' AND abci.owner.name = 'John'",
|
|
||||||
map[string][]string{"abci.owner.name": {"Igor", "Ivan"}},
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tm.events.type='NewBlock'",
|
|
||||||
map[string][]string{"tm.events.type": {"NewBlock"}, "app.name": {"fuzzed"}},
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"app.name = 'fuzzed'",
|
|
||||||
map[string][]string{"tm.events.type": {"NewBlock"}, "app.name": {"fuzzed"}},
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tm.events.type='NewBlock' AND app.name = 'fuzzed'",
|
|
||||||
map[string][]string{"tm.events.type": {"NewBlock"}, "app.name": {"fuzzed"}},
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"tm.events.type='NewHeader' AND app.name = 'fuzzed'",
|
|
||||||
map[string][]string{"tm.events.type": {"NewBlock"}, "app.name": {"fuzzed"}},
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{"slash EXISTS",
|
|
||||||
map[string][]string{"slash.reason": {"missing_signature"}, "slash.power": {"6000"}},
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{"sl EXISTS",
|
|
||||||
map[string][]string{"slash.reason": {"missing_signature"}, "slash.power": {"6000"}},
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{"slash EXISTS",
|
|
||||||
map[string][]string{"transfer.recipient": {"cosmos1gu6y2a0ffteesyeyeesk23082c6998xyzmt9mz"},
|
|
||||||
"transfer.sender": {"cosmos1crje20aj4gxdtyct7z3knxqry2jqt2fuaey6u5"}},
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{"slash.reason EXISTS AND slash.power > 1000",
|
|
||||||
map[string][]string{"slash.reason": {"missing_signature"}, "slash.power": {"6000"}},
|
|
||||||
true,
|
|
||||||
},
|
|
||||||
{"slash.reason EXISTS AND slash.power > 1000",
|
|
||||||
map[string][]string{"slash.reason": {"missing_signature"}, "slash.power": {"500"}},
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
{"slash.reason EXISTS",
|
|
||||||
map[string][]string{"transfer.recipient": {"cosmos1gu6y2a0ffteesyeyeesk23082c6998xyzmt9mz"},
|
|
||||||
"transfer.sender": {"cosmos1crje20aj4gxdtyct7z3knxqry2jqt2fuaey6u5"}},
|
|
||||||
false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
q, err := query.New(tc.s)
|
|
||||||
require.Nil(t, err)
|
|
||||||
require.NotNil(t, q, "Query '%s' should not be nil", tc.s)
|
|
||||||
|
|
||||||
match, err := q.Matches(tc.events)
|
|
||||||
require.Nil(t, err, "Query '%s' should not error on input %v", tc.s, tc.events)
|
|
||||||
require.Equal(t, tc.matches, match, "Query '%s' on input %v: got %v, want %v",
|
|
||||||
tc.s, tc.events, match, tc.matches)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMustParse(t *testing.T) {
|
|
||||||
assert.Panics(t, func() { query.MustParse("=") })
|
|
||||||
assert.NotPanics(t, func() { query.MustParse("tm.events.type='NewBlock'") })
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConditions(t *testing.T) {
|
|
||||||
txTime, err := time.Parse(time.RFC3339, "2013-05-03T14:45:00Z")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
s string
|
|
||||||
conditions []query.Condition
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
s: "tm.events.type='NewBlock'",
|
|
||||||
conditions: []query.Condition{
|
|
||||||
{CompositeKey: "tm.events.type", Op: query.OpEqual, Operand: "NewBlock"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
s: "tx.gas > 7 AND tx.gas < 9",
|
|
||||||
conditions: []query.Condition{
|
|
||||||
{CompositeKey: "tx.gas", Op: query.OpGreater, Operand: int64(7)},
|
|
||||||
{CompositeKey: "tx.gas", Op: query.OpLess, Operand: int64(9)},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
s: "tx.time >= TIME 2013-05-03T14:45:00Z",
|
|
||||||
conditions: []query.Condition{
|
|
||||||
{CompositeKey: "tx.time", Op: query.OpGreaterEqual, Operand: txTime},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
s: "slashing EXISTS",
|
|
||||||
conditions: []query.Condition{
|
|
||||||
{CompositeKey: "slashing", Op: query.OpExists},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
q, err := query.New(tc.s)
|
|
||||||
require.Nil(t, err)
|
|
||||||
|
|
||||||
c, err := q.Conditions()
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, tc.conditions, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user