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 | |||
import ( | |||
"fmt" | |||
"reflect" | |||
"regexp" | |||
"strconv" | |||
"strings" | |||
"time" | |||
"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 { | |||
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 &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 { | |||
panic(fmt.Sprintf("failed to parse %s: %v", s, err)) | |||
panic(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}) | |||
// 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: | |||
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) | |||
} | |||
} | |||
} | |||
} |