mirror of
https://github.com/tendermint/tendermint.git
synced 2025-12-23 06:15:19 +00:00
@@ -4,7 +4,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"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'`
|
||||
@@ -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) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, 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) {
|
||||
q, err := query.New(testQuery)
|
||||
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