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% ``` Components: * Add a basic parsing benchmark. * Move the original query implementation to a subdirectory. * Add lexical scanner for Query expressions. * Add a parser for Query expressions. * Implement query compiler. * Add test cases based on OpenAPI examples. * Add MustCompile to replace the original MustParse, and update usage.pull/7336/head
@ -0,0 +1,84 @@ | |||||
package query_test | |||||
import ( | |||||
"testing" | |||||
"github.com/tendermint/tendermint/abci/types" | |||||
"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'` | |||||
var testEvents = []types.Event{ | |||||
{ | |||||
Type: "tm.events", | |||||
Attributes: []types.EventAttribute{{ | |||||
Key: "index", | |||||
Value: "25", | |||||
}, { | |||||
Key: "type", | |||||
Value: "NewBlock", | |||||
}}, | |||||
}, | |||||
{ | |||||
Type: "abci.account", | |||||
Attributes: []types.EventAttribute{{ | |||||
Key: "name", | |||||
Value: "Anya", | |||||
}, { | |||||
Key: "name", | |||||
Value: "Igor", | |||||
}}, | |||||
}, | |||||
} | |||||
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) | |||||
if err != nil { | |||||
b.Fatal(err) | |||||
} | |||||
} | |||||
} | |||||
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 { | |||||
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") | |||||
} | |||||
} | |||||
} |
@ -0,0 +1,3 @@ | |||||
package query | |||||
//go:generate go run github.com/pointlander/peg@v1.0.0 -inline -switch query.peg |
@ -0,0 +1,527 @@ | |||||
// 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" | |||||
"github.com/tendermint/tendermint/abci/types" | |||||
) | |||||
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)} | |||||
p.Init() | |||||
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(rawEvents []types.Event) (bool, error) { | |||||
if len(rawEvents) == 0 { | |||||
return false, nil | |||||
} | |||||
events := flattenEvents(rawEvents) | |||||
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 | |||||
} | |||||
func flattenEvents(events []types.Event) map[string][]string { | |||||
flattened := make(map[string][]string) | |||||
for _, event := range events { | |||||
if len(event.Type) == 0 { | |||||
continue | |||||
} | |||||
for _, attr := range event.Attributes { | |||||
if len(attr.Key) == 0 { | |||||
continue | |||||
} | |||||
compositeEvent := fmt.Sprintf("%s.%s", event.Type, attr.Key) | |||||
flattened[compositeEvent] = append(flattened[compositeEvent], attr.Value) | |||||
} | |||||
} | |||||
return flattened | |||||
} |
@ -0,0 +1,205 @@ | |||||
package query_test | |||||
import ( | |||||
"fmt" | |||||
"strings" | |||||
"testing" | |||||
"time" | |||||
"github.com/stretchr/testify/require" | |||||
abci "github.com/tendermint/tendermint/abci/types" | |||||
query "github.com/tendermint/tendermint/libs/pubsub/query/oldquery" | |||||
) | |||||
func expandEvents(flattenedEvents map[string][]string) []abci.Event { | |||||
events := make([]abci.Event, len(flattenedEvents)) | |||||
for composite, values := range flattenedEvents { | |||||
tokens := strings.Split(composite, ".") | |||||
attrs := make([]abci.EventAttribute, len(values)) | |||||
for i, v := range values { | |||||
attrs[i] = abci.EventAttribute{ | |||||
Key: tokens[len(tokens)-1], | |||||
Value: v, | |||||
} | |||||
} | |||||
events = append(events, abci.Event{ | |||||
Type: strings.Join(tokens[:len(tokens)-1], "."), | |||||
Attributes: attrs, | |||||
}) | |||||
} | |||||
return events | |||||
} | |||||
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) | |||||
rawEvents := expandEvents(tc.events) | |||||
match, err := q.Matches(rawEvents) | |||||
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) { | |||||
require.Panics(t, func() { query.MustParse("=") }) | |||||
require.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) | |||||
require.Equal(t, tc.conditions, c) | |||||
} | |||||
} |
@ -1,3 +0,0 @@ | |||||
package query | |||||
//go:generate peg -inline -switch query.peg |
@ -1,527 +1,327 @@ | |||||
// Package query provides a parser for a custom query format: | |||||
// Package query implements the custom query format used to filter event | |||||
// subscriptions in Tendermint. | |||||
// | // | ||||
// abci.invoice.number=22 AND abci.invoice.owner=Ivan | |||||
// Query expressions describe properties of events and their attributes, using | |||||
// strings like: | |||||
// | // | ||||
// 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 | |||||
// abci.invoice.number = 22 AND abci.invoice.owner = 'Ivan' | |||||
// | |||||
// Query expressions can handle attribute values encoding numbers, strings, | |||||
// dates, and timestamps. The complete query grammar is described in the | |||||
// query/syntax package. | |||||
// | // | ||||
// It has a support for numbers (integer and floating point), dates and times. | |||||
package query | package query | ||||
import ( | import ( | ||||
"fmt" | "fmt" | ||||
"reflect" | |||||
"regexp" | "regexp" | ||||
"strconv" | "strconv" | ||||
"strings" | "strings" | ||||
"time" | "time" | ||||
"github.com/tendermint/tendermint/abci/types" | "github.com/tendermint/tendermint/abci/types" | ||||
"github.com/tendermint/tendermint/libs/pubsub/query/syntax" | |||||
) | ) | ||||
var ( | |||||
numRegex = regexp.MustCompile(`([0-9\.]+)`) | |||||
) | |||||
// All is a query that matches all events. | |||||
var All *Query | |||||
// Query holds the query string and the query parser. | |||||
// A Query is the compiled form of a query. | |||||
type Query struct { | type Query struct { | ||||
str string | |||||
parser *QueryParser | |||||
ast syntax.Query | |||||
conds []condition | |||||
} | } | ||||
// 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)} | |||||
p.Init() | |||||
if err := p.Parse(); err != nil { | |||||
// New parses and compiles the query expression into an executable query. | |||||
func New(query string) (*Query, error) { | |||||
ast, err := syntax.Parse(query) | |||||
if err != nil { | |||||
return nil, err | return nil, err | ||||
} | } | ||||
return &Query{str: s, parser: p}, nil | |||||
return Compile(ast) | |||||
} | } | ||||
// 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) | |||||
// MustCompile compiles the query expression into an executable query. | |||||
// In case of error, MustCompile will panic. | |||||
// | |||||
// This is intended for use in program initialization; use query.New if you | |||||
// need to check errors. | |||||
func MustCompile(query string) *Query { | |||||
q, err := New(query) | |||||
if err != nil { | if err != nil { | ||||
panic(fmt.Sprintf("failed to parse %s: %v", s, err)) | |||||
panic(err) | |||||
} | } | ||||
return q | 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}) | |||||
// Compile compiles the given query AST so it can be used to match events. | |||||
func Compile(ast syntax.Query) (*Query, error) { | |||||
conds := make([]condition, len(ast)) | |||||
for i, q := range ast { | |||||
cond, err := compileCondition(q) | |||||
if err != nil { | |||||
return nil, fmt.Errorf("compile %s: %w", q, err) | |||||
} | } | ||||
conds[i] = cond | |||||
} | } | ||||
return conditions, nil | |||||
return &Query{ast: ast, conds: conds}, 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(rawEvents []types.Event) (bool, error) { | |||||
if len(rawEvents) == 0 { | |||||
return false, nil | |||||
// Matches satisfies part of the pubsub.Query interface. This implementation | |||||
// never reports an error. A nil *Query matches all events. | |||||
func (q *Query) Matches(events []types.Event) (bool, error) { | |||||
if q == nil { | |||||
return true, nil | |||||
} | } | ||||
return q.matchesEvents(events), nil | |||||
} | |||||
events := flattenEvents(rawEvents) | |||||
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 | |||||
} | |||||
} | |||||
// String matches part of the pubsub.Query interface. | |||||
func (q *Query) String() string { | |||||
if q == nil { | |||||
return "<empty>" | |||||
} | } | ||||
return true, nil | |||||
return q.ast.String() | |||||
} | } | ||||
// 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 | |||||
// Syntax returns the syntax tree representation of q. | |||||
func (q *Query) Syntax() syntax.Query { | |||||
if q == nil { | |||||
return nil | |||||
} | } | ||||
return q.ast | |||||
} | |||||
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 | |||||
// matchesEvents reports whether all the conditions match the given events. | |||||
func (q *Query) matchesEvents(events []types.Event) bool { | |||||
for _, cond := range q.conds { | |||||
if !cond.matchesAny(events) { | |||||
return false | |||||
} | } | ||||
} | } | ||||
return false, nil | |||||
return len(events) != 0 | |||||
} | } | ||||
// 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 | |||||
) | |||||
// A condition is a compiled match condition. A condition matches an event if | |||||
// the event has the designated type, contains an attribute with the given | |||||
// name, and the match function returns true for the attribute value. | |||||
type condition struct { | |||||
tag string // e.g., "tx.hash" | |||||
match func(s string) bool | |||||
} | |||||
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) | |||||
// findAttr returns a slice of attribute values from event matching the | |||||
// condition tag, and reports whether the event type strictly equals the | |||||
// condition tag. | |||||
func (c condition) findAttr(event types.Event) ([]string, bool) { | |||||
if !strings.HasPrefix(c.tag, event.Type) { | |||||
return nil, false // type does not match tag | |||||
} else if len(c.tag) == len(event.Type) { | |||||
return nil, true // type == tag | |||||
} | |||||
var vals []string | |||||
for _, attr := range event.Attributes { | |||||
fullName := event.Type + "." + attr.Key | |||||
if fullName == c.tag { | |||||
vals = append(vals, attr.Value) | |||||
} | } | ||||
} | |||||
return vals, false | |||||
} | |||||
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 | |||||
// matchesAny reports whether c matches at least one of the given events. | |||||
func (c condition) matchesAny(events []types.Event) bool { | |||||
for _, event := range events { | |||||
if c.matchesEvent(event) { | |||||
return true | |||||
} | } | ||||
} | |||||
return false | |||||
} | |||||
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) | |||||
// matchesEvent reports whether c matches the given event. | |||||
func (c condition) matchesEvent(event types.Event) bool { | |||||
vs, tagEqualsType := c.findAttr(event) | |||||
if len(vs) == 0 { | |||||
// As a special case, a condition tag that exactly matches the event type | |||||
// is matched against an empty string. This allows existence checks to | |||||
// work for type-only queries. | |||||
if tagEqualsType { | |||||
return c.match("") | |||||
} | } | ||||
return false | |||||
} | |||||
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 | |||||
// At this point, we have candidate values. | |||||
for _, v := range vs { | |||||
if c.match(v) { | |||||
return true | |||||
} | } | ||||
} | |||||
return false | |||||
} | |||||
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) | |||||
} | |||||
} | |||||
func compileCondition(cond syntax.Condition) (condition, error) { | |||||
out := condition{tag: cond.Tag} | |||||
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 | |||||
} | |||||
// Handle existence checks separately to simplify the logic below for | |||||
// comparisons that take arguments. | |||||
if cond.Op == syntax.TExists { | |||||
out.match = func(string) bool { return true } | |||||
return out, nil | |||||
} | |||||
case reflect.String: | |||||
switch op { | |||||
case OpEqual: | |||||
return value == operand.String(), nil | |||||
case OpContains: | |||||
return strings.Contains(value, operand.String()), nil | |||||
} | |||||
// All the other operators require an argument. | |||||
if cond.Arg == nil { | |||||
return condition{}, fmt.Errorf("missing argument for %v", cond.Op) | |||||
} | |||||
// Precompile the argument value matcher. | |||||
argType := cond.Arg.Type | |||||
var argValue interface{} | |||||
switch argType { | |||||
case syntax.TString: | |||||
argValue = cond.Arg.Value() | |||||
case syntax.TNumber: | |||||
argValue = cond.Arg.Number() | |||||
case syntax.TTime, syntax.TDate: | |||||
argValue = cond.Arg.Time() | |||||
default: | default: | ||||
return false, fmt.Errorf("unknown kind of operand %v", operand.Kind()) | |||||
return condition{}, fmt.Errorf("unknown argument type %v", argType) | |||||
} | } | ||||
return false, nil | |||||
mcons := opTypeMap[cond.Op][argType] | |||||
if mcons == nil { | |||||
return condition{}, fmt.Errorf("invalid op/arg combination (%v, %v)", cond.Op, argType) | |||||
} | |||||
out.match = mcons(argValue) | |||||
return out, nil | |||||
} | } | ||||
func flattenEvents(events []types.Event) map[string][]string { | |||||
flattened := make(map[string][]string) | |||||
// TODO(creachadair): The existing implementation allows anything number shaped | |||||
// to be treated as a number. This preserves the parts of that behavior we had | |||||
// tests for, but we should probably get rid of that. | |||||
var extractNum = regexp.MustCompile(`^\d+(\.\d+)?`) | |||||
for _, event := range events { | |||||
if len(event.Type) == 0 { | |||||
continue | |||||
} | |||||
func parseNumber(s string) (float64, error) { | |||||
return strconv.ParseFloat(extractNum.FindString(s), 64) | |||||
} | |||||
for _, attr := range event.Attributes { | |||||
if len(attr.Key) == 0 { | |||||
continue | |||||
// A map of operator ⇒ argtype ⇒ match-constructor. | |||||
// An entry does not exist if the combination is not valid. | |||||
// | |||||
// Disable the dupl lint for this map. The result isn't even correct. | |||||
//nolint:dupl | |||||
var opTypeMap = map[syntax.Token]map[syntax.Token]func(interface{}) func(string) bool{ | |||||
syntax.TContains: { | |||||
syntax.TString: func(v interface{}) func(string) bool { | |||||
return func(s string) bool { | |||||
return strings.Contains(s, v.(string)) | |||||
} | } | ||||
compositeEvent := fmt.Sprintf("%s.%s", event.Type, attr.Key) | |||||
flattened[compositeEvent] = append(flattened[compositeEvent], attr.Value) | |||||
} | |||||
} | |||||
return flattened | |||||
}, | |||||
}, | |||||
syntax.TEq: { | |||||
syntax.TString: func(v interface{}) func(string) bool { | |||||
return func(s string) bool { return s == v.(string) } | |||||
}, | |||||
syntax.TNumber: func(v interface{}) func(string) bool { | |||||
return func(s string) bool { | |||||
w, err := parseNumber(s) | |||||
return err == nil && w == v.(float64) | |||||
} | |||||
}, | |||||
syntax.TDate: func(v interface{}) func(string) bool { | |||||
return func(s string) bool { | |||||
ts, err := syntax.ParseDate(s) | |||||
return err == nil && ts.Equal(v.(time.Time)) | |||||
} | |||||
}, | |||||
syntax.TTime: func(v interface{}) func(string) bool { | |||||
return func(s string) bool { | |||||
ts, err := syntax.ParseTime(s) | |||||
return err == nil && ts.Equal(v.(time.Time)) | |||||
} | |||||
}, | |||||
}, | |||||
syntax.TLt: { | |||||
syntax.TNumber: func(v interface{}) func(string) bool { | |||||
return func(s string) bool { | |||||
w, err := parseNumber(s) | |||||
return err == nil && w < v.(float64) | |||||
} | |||||
}, | |||||
syntax.TDate: func(v interface{}) func(string) bool { | |||||
return func(s string) bool { | |||||
ts, err := syntax.ParseDate(s) | |||||
return err == nil && ts.Before(v.(time.Time)) | |||||
} | |||||
}, | |||||
syntax.TTime: func(v interface{}) func(string) bool { | |||||
return func(s string) bool { | |||||
ts, err := syntax.ParseTime(s) | |||||
return err == nil && ts.Before(v.(time.Time)) | |||||
} | |||||
}, | |||||
}, | |||||
syntax.TLeq: { | |||||
syntax.TNumber: func(v interface{}) func(string) bool { | |||||
return func(s string) bool { | |||||
w, err := parseNumber(s) | |||||
return err == nil && w <= v.(float64) | |||||
} | |||||
}, | |||||
syntax.TDate: func(v interface{}) func(string) bool { | |||||
return func(s string) bool { | |||||
ts, err := syntax.ParseDate(s) | |||||
return err == nil && !ts.After(v.(time.Time)) | |||||
} | |||||
}, | |||||
syntax.TTime: func(v interface{}) func(string) bool { | |||||
return func(s string) bool { | |||||
ts, err := syntax.ParseTime(s) | |||||
return err == nil && !ts.After(v.(time.Time)) | |||||
} | |||||
}, | |||||
}, | |||||
syntax.TGt: { | |||||
syntax.TNumber: func(v interface{}) func(string) bool { | |||||
return func(s string) bool { | |||||
w, err := parseNumber(s) | |||||
return err == nil && w > v.(float64) | |||||
} | |||||
}, | |||||
syntax.TDate: func(v interface{}) func(string) bool { | |||||
return func(s string) bool { | |||||
ts, err := syntax.ParseDate(s) | |||||
return err == nil && ts.After(v.(time.Time)) | |||||
} | |||||
}, | |||||
syntax.TTime: func(v interface{}) func(string) bool { | |||||
return func(s string) bool { | |||||
ts, err := syntax.ParseTime(s) | |||||
return err == nil && ts.After(v.(time.Time)) | |||||
} | |||||
}, | |||||
}, | |||||
syntax.TGeq: { | |||||
syntax.TNumber: func(v interface{}) func(string) bool { | |||||
return func(s string) bool { | |||||
w, err := parseNumber(s) | |||||
return err == nil && w >= v.(float64) | |||||
} | |||||
}, | |||||
syntax.TDate: func(v interface{}) func(string) bool { | |||||
return func(s string) bool { | |||||
ts, err := syntax.ParseDate(s) | |||||
return err == nil && !ts.Before(v.(time.Time)) | |||||
} | |||||
}, | |||||
syntax.TTime: func(v interface{}) func(string) bool { | |||||
return func(s string) bool { | |||||
ts, err := syntax.ParseTime(s) | |||||
return err == nil && !ts.Before(v.(time.Time)) | |||||
} | |||||
}, | |||||
}, | |||||
} | } |
@ -0,0 +1,34 @@ | |||||
// Package syntax defines a scanner and parser for the Tendermint event filter | |||||
// query language. A query selects events by their types and attribute values. | |||||
// | |||||
// Grammar | |||||
// | |||||
// The grammar of the query language is defined by the following EBNF: | |||||
// | |||||
// query = conditions EOF | |||||
// conditions = condition {"AND" condition} | |||||
// condition = tag comparison | |||||
// comparison = equal / order / contains / "EXISTS" | |||||
// equal = "=" (date / number / time / value) | |||||
// order = cmp (date / number / time) | |||||
// contains = "CONTAINS" value | |||||
// cmp = "<" / "<=" / ">" / ">=" | |||||
// | |||||
// The lexical terms are defined here using RE2 regular expression notation: | |||||
// | |||||
// // The name of an event attribute (type.value) | |||||
// tag = #'\w+(\.\w+)*' | |||||
// | |||||
// // A datestamp (YYYY-MM-DD) | |||||
// date = #'DATE \d{4}-\d{2}-\d{2}' | |||||
// | |||||
// // A number with optional fractional parts (0, 10, 3.25) | |||||
// number = #'\d+(\.\d+)?' | |||||
// | |||||
// // An RFC3339 timestamp (2021-11-23T22:04:19-09:00) | |||||
// time = #'TIME \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}([-+]\d{2}:\d{2}|Z)' | |||||
// | |||||
// // A quoted literal string value ('a b c') | |||||
// value = #'\'[^\']*\'' | |||||
// | |||||
package syntax |
@ -0,0 +1,213 @@ | |||||
package syntax | |||||
import ( | |||||
"fmt" | |||||
"io" | |||||
"math" | |||||
"strconv" | |||||
"strings" | |||||
"time" | |||||
) | |||||
// Parse parses the specified query string. It is shorthand for constructing a | |||||
// parser for s and calling its Parse method. | |||||
func Parse(s string) (Query, error) { | |||||
return NewParser(strings.NewReader(s)).Parse() | |||||
} | |||||
// Query is the root of the parse tree for a query. A query is the conjunction | |||||
// of one or more conditions. | |||||
type Query []Condition | |||||
func (q Query) String() string { | |||||
ss := make([]string, len(q)) | |||||
for i, cond := range q { | |||||
ss[i] = cond.String() | |||||
} | |||||
return strings.Join(ss, " AND ") | |||||
} | |||||
// A Condition is a single conditional expression, consisting of a tag, a | |||||
// comparison operator, and an optional argument. The type of the argument | |||||
// depends on the operator. | |||||
type Condition struct { | |||||
Tag string | |||||
Op Token | |||||
Arg *Arg | |||||
opText string | |||||
} | |||||
func (c Condition) String() string { | |||||
s := c.Tag + " " + c.opText | |||||
if c.Arg != nil { | |||||
return s + " " + c.Arg.String() | |||||
} | |||||
return s | |||||
} | |||||
// An Arg is the argument of a comparison operator. | |||||
type Arg struct { | |||||
Type Token | |||||
text string | |||||
} | |||||
func (a *Arg) String() string { | |||||
if a == nil { | |||||
return "" | |||||
} | |||||
switch a.Type { | |||||
case TString: | |||||
return "'" + a.text + "'" | |||||
case TTime: | |||||
return "TIME " + a.text | |||||
case TDate: | |||||
return "DATE " + a.text | |||||
default: | |||||
return a.text | |||||
} | |||||
} | |||||
// Number returns the value of the argument text as a number, or a NaN if the | |||||
// text does not encode a valid number value. | |||||
func (a *Arg) Number() float64 { | |||||
if a == nil { | |||||
return -1 | |||||
} | |||||
v, err := strconv.ParseFloat(a.text, 64) | |||||
if err == nil && v >= 0 { | |||||
return v | |||||
} | |||||
return math.NaN() | |||||
} | |||||
// Time returns the value of the argument text as a time, or the zero value if | |||||
// the text does not encode a timestamp or datestamp. | |||||
func (a *Arg) Time() time.Time { | |||||
var ts time.Time | |||||
if a == nil { | |||||
return ts | |||||
} | |||||
var err error | |||||
switch a.Type { | |||||
case TDate: | |||||
ts, err = ParseDate(a.text) | |||||
case TTime: | |||||
ts, err = ParseTime(a.text) | |||||
} | |||||
if err == nil { | |||||
return ts | |||||
} | |||||
return time.Time{} | |||||
} | |||||
// Value returns the value of the argument text as a string, or "". | |||||
func (a *Arg) Value() string { | |||||
if a == nil { | |||||
return "" | |||||
} | |||||
return a.text | |||||
} | |||||
// Parser is a query expression parser. The grammar for query expressions is | |||||
// defined in the syntax package documentation. | |||||
type Parser struct { | |||||
scanner *Scanner | |||||
} | |||||
// NewParser constructs a new parser that reads the input from r. | |||||
func NewParser(r io.Reader) *Parser { | |||||
return &Parser{scanner: NewScanner(r)} | |||||
} | |||||
// Parse parses the complete input and returns the resulting query. | |||||
func (p *Parser) Parse() (Query, error) { | |||||
cond, err := p.parseCond() | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
conds := []Condition{cond} | |||||
for p.scanner.Next() != io.EOF { | |||||
if tok := p.scanner.Token(); tok != TAnd { | |||||
return nil, fmt.Errorf("offset %d: got %v, want %v", p.scanner.Pos(), tok, TAnd) | |||||
} | |||||
cond, err := p.parseCond() | |||||
if err != nil { | |||||
return nil, err | |||||
} | |||||
conds = append(conds, cond) | |||||
} | |||||
return conds, nil | |||||
} | |||||
// parseCond parses a conditional expression: tag OP value. | |||||
func (p *Parser) parseCond() (Condition, error) { | |||||
var cond Condition | |||||
if err := p.require(TTag); err != nil { | |||||
return cond, err | |||||
} | |||||
cond.Tag = p.scanner.Text() | |||||
if err := p.require(TLeq, TGeq, TLt, TGt, TEq, TContains, TExists); err != nil { | |||||
return cond, err | |||||
} | |||||
cond.Op = p.scanner.Token() | |||||
cond.opText = p.scanner.Text() | |||||
var err error | |||||
switch cond.Op { | |||||
case TLeq, TGeq, TLt, TGt: | |||||
err = p.require(TNumber, TTime, TDate) | |||||
case TEq: | |||||
err = p.require(TNumber, TTime, TDate, TString) | |||||
case TContains: | |||||
err = p.require(TString) | |||||
case TExists: | |||||
// no argument | |||||
return cond, nil | |||||
default: | |||||
return cond, fmt.Errorf("offset %d: unexpected operator %v", p.scanner.Pos(), cond.Op) | |||||
} | |||||
if err != nil { | |||||
return cond, err | |||||
} | |||||
cond.Arg = &Arg{Type: p.scanner.Token(), text: p.scanner.Text()} | |||||
return cond, nil | |||||
} | |||||
// require advances the scanner and requires that the resulting token is one of | |||||
// the specified token types. | |||||
func (p *Parser) require(tokens ...Token) error { | |||||
if err := p.scanner.Next(); err != nil { | |||||
return fmt.Errorf("offset %d: %w", p.scanner.Pos(), err) | |||||
} | |||||
got := p.scanner.Token() | |||||
for _, tok := range tokens { | |||||
if tok == got { | |||||
return nil | |||||
} | |||||
} | |||||
return fmt.Errorf("offset %d: got %v, wanted %s", p.scanner.Pos(), got, tokLabel(tokens)) | |||||
} | |||||
// tokLabel makes a human-readable summary string for the given token types. | |||||
func tokLabel(tokens []Token) string { | |||||
if len(tokens) == 1 { | |||||
return tokens[0].String() | |||||
} | |||||
last := len(tokens) - 1 | |||||
ss := make([]string, len(tokens)-1) | |||||
for i, tok := range tokens[:last] { | |||||
ss[i] = tok.String() | |||||
} | |||||
return strings.Join(ss, ", ") + " or " + tokens[last].String() | |||||
} | |||||
// ParseDate parses s as a date string in the format used by DATE values. | |||||
func ParseDate(s string) (time.Time, error) { | |||||
return time.Parse("2006-01-02", s) | |||||
} | |||||
// ParseTime parses s as a timestamp in the format used by TIME values. | |||||
func ParseTime(s string) (time.Time, error) { | |||||
return time.Parse(time.RFC3339, s) | |||||
} |
@ -0,0 +1,312 @@ | |||||
package syntax | |||||
import ( | |||||
"bufio" | |||||
"bytes" | |||||
"fmt" | |||||
"io" | |||||
"strings" | |||||
"time" | |||||
"unicode" | |||||
) | |||||
// Token is the type of a lexical token in the query grammar. | |||||
type Token byte | |||||
const ( | |||||
TInvalid = iota // invalid or unknown token | |||||
TTag // field tag: x.y | |||||
TString // string value: 'foo bar' | |||||
TNumber // number: 0, 15.5, 100 | |||||
TTime // timestamp: TIME yyyy-mm-ddThh:mm:ss([-+]hh:mm|Z) | |||||
TDate // datestamp: DATE yyyy-mm-dd | |||||
TAnd // operator: AND | |||||
TContains // operator: CONTAINS | |||||
TExists // operator: EXISTS | |||||
TEq // operator: = | |||||
TLt // operator: < | |||||
TLeq // operator: <= | |||||
TGt // operator: > | |||||
TGeq // operator: >= | |||||
// Do not reorder these values without updating the scanner code. | |||||
) | |||||
var tString = [...]string{ | |||||
TInvalid: "invalid token", | |||||
TTag: "tag", | |||||
TString: "string", | |||||
TNumber: "number", | |||||
TTime: "timestamp", | |||||
TDate: "datestamp", | |||||
TAnd: "AND operator", | |||||
TContains: "CONTAINS operator", | |||||
TExists: "EXISTS operator", | |||||
TEq: "= operator", | |||||
TLt: "< operator", | |||||
TLeq: "<= operator", | |||||
TGt: "> operator", | |||||
TGeq: ">= operator", | |||||
} | |||||
func (t Token) String() string { | |||||
v := int(t) | |||||
if v > len(tString) { | |||||
return "unknown token type" | |||||
} | |||||
return tString[v] | |||||
} | |||||
const ( | |||||
// TimeFormat is the format string used for timestamp values. | |||||
TimeFormat = time.RFC3339 | |||||
// DateFormat is the format string used for datestamp values. | |||||
DateFormat = "2006-01-02" | |||||
) | |||||
// Scanner reads lexical tokens of the query language from an input stream. | |||||
// Each call to Next advances the scanner to the next token, or reports an | |||||
// error. | |||||
type Scanner struct { | |||||
r *bufio.Reader | |||||
buf bytes.Buffer | |||||
tok Token | |||||
err error | |||||
pos, last, end int | |||||
} | |||||
// NewScanner constructs a new scanner that reads from r. | |||||
func NewScanner(r io.Reader) *Scanner { return &Scanner{r: bufio.NewReader(r)} } | |||||
// Next advances s to the next token in the input, or reports an error. At the | |||||
// end of input, Next returns io.EOF. | |||||
func (s *Scanner) Next() error { | |||||
s.buf.Reset() | |||||
s.pos = s.end | |||||
s.tok = TInvalid | |||||
s.err = nil | |||||
for { | |||||
ch, err := s.rune() | |||||
if err != nil { | |||||
return s.fail(err) | |||||
} | |||||
if unicode.IsSpace(ch) { | |||||
s.pos = s.end | |||||
continue // skip whitespace | |||||
} | |||||
if '0' <= ch && ch <= '9' { | |||||
return s.scanNumber(ch) | |||||
} else if isTagRune(ch) { | |||||
return s.scanTagLike(ch) | |||||
} | |||||
switch ch { | |||||
case '\'': | |||||
return s.scanString(ch) | |||||
case '<', '>', '=': | |||||
return s.scanCompare(ch) | |||||
default: | |||||
return s.invalid(ch) | |||||
} | |||||
} | |||||
} | |||||
// Token returns the type of the current input token. | |||||
func (s *Scanner) Token() Token { return s.tok } | |||||
// Text returns the text of the current input token. | |||||
func (s *Scanner) Text() string { return s.buf.String() } | |||||
// Pos returns the start offset of the current token in the input. | |||||
func (s *Scanner) Pos() int { return s.pos } | |||||
// Err returns the last error reported by Next, if any. | |||||
func (s *Scanner) Err() error { return s.err } | |||||
// scanNumber scans for numbers with optional fractional parts. | |||||
// Examples: 0, 1, 3.14 | |||||
func (s *Scanner) scanNumber(first rune) error { | |||||
s.buf.WriteRune(first) | |||||
if err := s.scanWhile(isDigit); err != nil { | |||||
return err | |||||
} | |||||
ch, err := s.rune() | |||||
if err != nil && err != io.EOF { | |||||
return err | |||||
} | |||||
if ch == '.' { | |||||
s.buf.WriteRune(ch) | |||||
if err := s.scanWhile(isDigit); err != nil { | |||||
return err | |||||
} | |||||
} else { | |||||
s.unrune() | |||||
} | |||||
s.tok = TNumber | |||||
return nil | |||||
} | |||||
func (s *Scanner) scanString(first rune) error { | |||||
// discard opening quote | |||||
for { | |||||
ch, err := s.rune() | |||||
if err != nil { | |||||
return s.fail(err) | |||||
} else if ch == first { | |||||
// discard closing quote | |||||
s.tok = TString | |||||
return nil | |||||
} | |||||
s.buf.WriteRune(ch) | |||||
} | |||||
} | |||||
func (s *Scanner) scanCompare(first rune) error { | |||||
s.buf.WriteRune(first) | |||||
switch first { | |||||
case '=': | |||||
s.tok = TEq | |||||
return nil | |||||
case '<': | |||||
s.tok = TLt | |||||
case '>': | |||||
s.tok = TGt | |||||
default: | |||||
return s.invalid(first) | |||||
} | |||||
ch, err := s.rune() | |||||
if err == io.EOF { | |||||
return nil // the assigned token is correct | |||||
} else if err != nil { | |||||
return s.fail(err) | |||||
} | |||||
if ch == '=' { | |||||
s.buf.WriteRune(ch) | |||||
s.tok++ // depends on token order | |||||
return nil | |||||
} | |||||
s.unrune() | |||||
return nil | |||||
} | |||||
func (s *Scanner) scanTagLike(first rune) error { | |||||
s.buf.WriteRune(first) | |||||
var hasSpace bool | |||||
for { | |||||
ch, err := s.rune() | |||||
if err == io.EOF { | |||||
break | |||||
} else if err != nil { | |||||
return s.fail(err) | |||||
} | |||||
if !isTagRune(ch) { | |||||
hasSpace = ch == ' ' // to check for TIME, DATE | |||||
break | |||||
} | |||||
s.buf.WriteRune(ch) | |||||
} | |||||
text := s.buf.String() | |||||
switch text { | |||||
case "TIME": | |||||
if hasSpace { | |||||
return s.scanTimestamp() | |||||
} | |||||
s.tok = TTag | |||||
case "DATE": | |||||
if hasSpace { | |||||
return s.scanDatestamp() | |||||
} | |||||
s.tok = TTag | |||||
case "AND": | |||||
s.tok = TAnd | |||||
case "EXISTS": | |||||
s.tok = TExists | |||||
case "CONTAINS": | |||||
s.tok = TContains | |||||
default: | |||||
s.tok = TTag | |||||
} | |||||
s.unrune() | |||||
return nil | |||||
} | |||||
func (s *Scanner) scanTimestamp() error { | |||||
s.buf.Reset() // discard "TIME" label | |||||
if err := s.scanWhile(isTimeRune); err != nil { | |||||
return err | |||||
} | |||||
if ts, err := time.Parse(TimeFormat, s.buf.String()); err != nil { | |||||
return s.fail(fmt.Errorf("invalid TIME value: %w", err)) | |||||
} else if y := ts.Year(); y < 1900 || y > 2999 { | |||||
return s.fail(fmt.Errorf("timestamp year %d out of range", ts.Year())) | |||||
} | |||||
s.tok = TTime | |||||
return nil | |||||
} | |||||
func (s *Scanner) scanDatestamp() error { | |||||
s.buf.Reset() // discard "DATE" label | |||||
if err := s.scanWhile(isDateRune); err != nil { | |||||
return err | |||||
} | |||||
if ts, err := time.Parse(DateFormat, s.buf.String()); err != nil { | |||||
return s.fail(fmt.Errorf("invalid DATE value: %w", err)) | |||||
} else if y := ts.Year(); y < 1900 || y > 2999 { | |||||
return s.fail(fmt.Errorf("datestamp year %d out of range", ts.Year())) | |||||
} | |||||
s.tok = TDate | |||||
return nil | |||||
} | |||||
func (s *Scanner) scanWhile(ok func(rune) bool) error { | |||||
for { | |||||
ch, err := s.rune() | |||||
if err == io.EOF { | |||||
return nil | |||||
} else if err != nil { | |||||
return s.fail(err) | |||||
} else if !ok(ch) { | |||||
s.unrune() | |||||
return nil | |||||
} | |||||
s.buf.WriteRune(ch) | |||||
} | |||||
} | |||||
func (s *Scanner) rune() (rune, error) { | |||||
ch, nb, err := s.r.ReadRune() | |||||
s.last = nb | |||||
s.end += nb | |||||
return ch, err | |||||
} | |||||
func (s *Scanner) unrune() { | |||||
_ = s.r.UnreadRune() | |||||
s.end -= s.last | |||||
} | |||||
func (s *Scanner) fail(err error) error { | |||||
s.err = err | |||||
return err | |||||
} | |||||
func (s *Scanner) invalid(ch rune) error { | |||||
return s.fail(fmt.Errorf("invalid input %c at offset %d", ch, s.end)) | |||||
} | |||||
func isDigit(r rune) bool { return '0' <= r && r <= '9' } | |||||
func isTagRune(r rune) bool { | |||||
return r == '.' || r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r) | |||||
} | |||||
func isTimeRune(r rune) bool { | |||||
return strings.ContainsRune("-T:+Z", r) || isDigit(r) | |||||
} | |||||
func isDateRune(r rune) bool { return isDigit(r) || r == '-' } |
@ -0,0 +1,190 @@ | |||||
package syntax_test | |||||
import ( | |||||
"io" | |||||
"reflect" | |||||
"strings" | |||||
"testing" | |||||
"github.com/tendermint/tendermint/libs/pubsub/query/syntax" | |||||
) | |||||
func TestScanner(t *testing.T) { | |||||
tests := []struct { | |||||
input string | |||||
want []syntax.Token | |||||
}{ | |||||
// Empty inputs | |||||
{"", nil}, | |||||
{" ", nil}, | |||||
{"\t\n ", nil}, | |||||
// Numbers | |||||
{`0 123`, []syntax.Token{syntax.TNumber, syntax.TNumber}}, | |||||
{`0.32 3.14`, []syntax.Token{syntax.TNumber, syntax.TNumber}}, | |||||
// Tags | |||||
{`foo foo.bar`, []syntax.Token{syntax.TTag, syntax.TTag}}, | |||||
// Strings (values) | |||||
{` '' x 'x' 'x y'`, []syntax.Token{syntax.TString, syntax.TTag, syntax.TString, syntax.TString}}, | |||||
{` 'you are not your job' `, []syntax.Token{syntax.TString}}, | |||||
// Comparison operators | |||||
{`< <= = > >=`, []syntax.Token{ | |||||
syntax.TLt, syntax.TLeq, syntax.TEq, syntax.TGt, syntax.TGeq, | |||||
}}, | |||||
// Mixed values of various kinds. | |||||
{`x AND y`, []syntax.Token{syntax.TTag, syntax.TAnd, syntax.TTag}}, | |||||
{`x.y CONTAINS 'z'`, []syntax.Token{syntax.TTag, syntax.TContains, syntax.TString}}, | |||||
{`foo EXISTS`, []syntax.Token{syntax.TTag, syntax.TExists}}, | |||||
{`and AND`, []syntax.Token{syntax.TTag, syntax.TAnd}}, | |||||
// Timestamp | |||||
{`TIME 2021-11-23T15:16:17Z`, []syntax.Token{syntax.TTime}}, | |||||
// Datestamp | |||||
{`DATE 2021-11-23`, []syntax.Token{syntax.TDate}}, | |||||
} | |||||
for _, test := range tests { | |||||
s := syntax.NewScanner(strings.NewReader(test.input)) | |||||
var got []syntax.Token | |||||
for s.Next() == nil { | |||||
got = append(got, s.Token()) | |||||
} | |||||
if err := s.Err(); err != io.EOF { | |||||
t.Errorf("Next: unexpected error: %v", err) | |||||
} | |||||
if !reflect.DeepEqual(got, test.want) { | |||||
t.Logf("Scanner input: %q", test.input) | |||||
t.Errorf("Wrong tokens:\ngot: %+v\nwant: %+v", got, test.want) | |||||
} | |||||
} | |||||
} | |||||
func TestScannerErrors(t *testing.T) { | |||||
tests := []struct { | |||||
input string | |||||
}{ | |||||
{`'incomplete string`}, | |||||
{`-23`}, | |||||
{`&`}, | |||||
{`DATE xyz-pdq`}, | |||||
{`DATE xyzp-dq-zv`}, | |||||
{`DATE 0000-00-00`}, | |||||
{`DATE 0000-00-000`}, | |||||
{`DATE 2021-01-99`}, | |||||
{`TIME 2021-01-01T34:56:78Z`}, | |||||
{`TIME 2021-01-99T14:56:08Z`}, | |||||
{`TIME 2021-01-99T34:56:08`}, | |||||
{`TIME 2021-01-99T34:56:11+3`}, | |||||
} | |||||
for _, test := range tests { | |||||
s := syntax.NewScanner(strings.NewReader(test.input)) | |||||
if err := s.Next(); err == nil { | |||||
t.Errorf("Next: got %v (%#q), want error", s.Token(), s.Text()) | |||||
} | |||||
} | |||||
} | |||||
// These parser tests were copied from the original implementation of the query | |||||
// parser, and are preserved here as a compatibility check. | |||||
func TestParseValid(t *testing.T) { | |||||
tests := []struct { | |||||
input 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 _, test := range tests { | |||||
q, err := syntax.Parse(test.input) | |||||
if test.valid != (err == nil) { | |||||
t.Errorf("Parse %#q: valid %v got err=%v", test.input, test.valid, err) | |||||
} | |||||
// For valid queries, check that the query round-trips. | |||||
if test.valid { | |||||
qstr := q.String() | |||||
r, err := syntax.Parse(qstr) | |||||
if err != nil { | |||||
t.Errorf("Reparse %#q failed: %v", qstr, err) | |||||
} | |||||
if rstr := r.String(); rstr != qstr { | |||||
t.Errorf("Reparse diff\nold: %#q\nnew: %#q", qstr, rstr) | |||||
} | |||||
} | |||||
} | |||||
} |