mirror of
https://github.com/tendermint/tendermint.git
synced 2026-01-07 22:05:18 +00:00
* Rename Tag(s) to Event(s) - tag was replaced with event, but in some places it still mentions tag, would be easier to understand if we tried to replace it with event to not confuse people. Signed-off-by: Marko Baricevic <marbar3778@yahoo.com> * more changes from tag -> event * rename events to compositeKeys and keys * Apply suggestions from code review Co-Authored-By: Anton Kaliaev <anton.kalyaev@gmail.com> * add minor documentation on how composite keys are constructed * rename eventkey to compositekey Co-Authored-By: Anton Kaliaev <anton.kalyaev@gmail.com> * add changelog entry & add info to regenerate confid to changelog entry
505 lines
12 KiB
Go
505 lines
12 KiB
Go
// 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/pkg/errors"
|
|
)
|
|
|
|
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(events map[string][]string) (bool, error) {
|
|
if len(events) == 0 {
|
|
return false, nil
|
|
}
|
|
|
|
var (
|
|
eventAttr string
|
|
op Operator
|
|
)
|
|
|
|
buffer, begin, end := q.parser.Buffer, 0, 0
|
|
|
|
// tokens must be in the following order:
|
|
|
|
// tag ("tx.gas") -> operator ("=") -> operand ("7")
|
|
for token := range q.parser.Tokens() {
|
|
switch token.pegRule {
|
|
case rulePegText:
|
|
begin, end = int(token.begin), int(token.end)
|
|
|
|
case ruletag:
|
|
eventAttr = buffer[begin:end]
|
|
|
|
case rulele:
|
|
op = OpLessEqual
|
|
|
|
case rulege:
|
|
op = OpGreaterEqual
|
|
|
|
case rulel:
|
|
op = OpLess
|
|
|
|
case ruleg:
|
|
op = OpGreater
|
|
|
|
case ruleequal:
|
|
op = OpEqual
|
|
|
|
case rulecontains:
|
|
op = OpContains
|
|
case ruleexists:
|
|
op = OpExists
|
|
if strings.Contains(eventAttr, ".") {
|
|
// Searching for a full "type.attribute" event.
|
|
_, ok := events[eventAttr]
|
|
if !ok {
|
|
return false, nil
|
|
}
|
|
} else {
|
|
foundEvent := false
|
|
|
|
loop:
|
|
for compositeKey := range events {
|
|
if strings.Index(compositeKey, eventAttr) == 0 {
|
|
foundEvent = true
|
|
break loop
|
|
}
|
|
}
|
|
if !foundEvent {
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
case rulevalue:
|
|
// strip single quotes from value (i.e. "'NewBlock'" -> "NewBlock")
|
|
valueWithoutSingleQuotes := buffer[begin+1 : end-1]
|
|
|
|
// see if the triplet (event attribute, operator, operand) matches any event
|
|
// "tx.gas", "=", "7", { "tx.gas": 7, "tx.ID": "4AE393495334" }
|
|
match, err := match(eventAttr, op, reflect.ValueOf(valueWithoutSingleQuotes), events)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if !match {
|
|
return false, nil
|
|
}
|
|
|
|
case rulenumber:
|
|
number := buffer[begin:end]
|
|
if strings.ContainsAny(number, ".") { // if it looks like a floating-point number
|
|
value, err := strconv.ParseFloat(number, 64)
|
|
if err != nil {
|
|
err = fmt.Errorf(
|
|
"got %v while trying to parse %s as float64 (should never happen if the grammar is correct)",
|
|
err, number,
|
|
)
|
|
return false, err
|
|
}
|
|
|
|
match, err := match(eventAttr, op, reflect.ValueOf(value), events)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if !match {
|
|
return false, nil
|
|
}
|
|
} else {
|
|
value, err := strconv.ParseInt(number, 10, 64)
|
|
if err != nil {
|
|
err = fmt.Errorf(
|
|
"got %v while trying to parse %s as int64 (should never happen if the grammar is correct)",
|
|
err, number,
|
|
)
|
|
return false, err
|
|
}
|
|
|
|
match, err := match(eventAttr, op, reflect.ValueOf(value), events)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if !match {
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
case ruletime:
|
|
value, err := time.Parse(TimeLayout, buffer[begin:end])
|
|
if err != nil {
|
|
err = fmt.Errorf(
|
|
"got %v while trying to parse %s as time.Time / RFC3339 (should never happen if the grammar is correct)",
|
|
err, buffer[begin:end],
|
|
)
|
|
return false, err
|
|
}
|
|
|
|
match, err := match(eventAttr, op, reflect.ValueOf(value), events)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if !match {
|
|
return false, nil
|
|
}
|
|
|
|
case ruledate:
|
|
value, err := time.Parse("2006-01-02", buffer[begin:end])
|
|
if err != nil {
|
|
err = fmt.Errorf(
|
|
"got %v while trying to parse %s as time.Time / '2006-01-02' (should never happen if the grammar is correct)",
|
|
err, buffer[begin:end],
|
|
)
|
|
return false, err
|
|
}
|
|
|
|
match, err := match(eventAttr, op, reflect.ValueOf(value), events)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if !match {
|
|
return false, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
// match returns true if the given triplet (attribute, operator, operand) matches
|
|
// any value in an event for that attribute. If any match fails with an error,
|
|
// that error is returned.
|
|
//
|
|
// First, it looks up the key in the events and if it finds one, tries to compare
|
|
// all the values from it to the operand using the operator.
|
|
//
|
|
// "tx.gas", "=", "7", {"tx": [{"gas": 7, "ID": "4AE393495334"}]}
|
|
func match(attr string, op Operator, operand reflect.Value, events map[string][]string) (bool, error) {
|
|
// look up the tag from the query in tags
|
|
values, ok := events[attr]
|
|
if !ok {
|
|
return false, nil
|
|
}
|
|
|
|
for _, value := range values {
|
|
// return true if any value in the set of the event's values matches
|
|
match, err := matchValue(value, op, operand)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if match {
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// matchValue will attempt to match a string value against an operator an
|
|
// operand. A boolean is returned representing the match result. It will return
|
|
// an error if the value cannot be parsed and matched against the operand type.
|
|
func matchValue(value string, op Operator, operand reflect.Value) (bool, error) {
|
|
switch operand.Kind() {
|
|
case reflect.Struct: // time
|
|
operandAsTime := operand.Interface().(time.Time)
|
|
|
|
// try our best to convert value from events to time.Time
|
|
var (
|
|
v time.Time
|
|
err error
|
|
)
|
|
|
|
if strings.ContainsAny(value, "T") {
|
|
v, err = time.Parse(TimeLayout, value)
|
|
} else {
|
|
v, err = time.Parse(DateLayout, value)
|
|
}
|
|
if err != nil {
|
|
return false, errors.Wrapf(err, "failed to convert value %v from event attribute to time.Time", value)
|
|
}
|
|
|
|
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, errors.Wrapf(err, "failed to convert value %v from event attribute to float64", filteredValue)
|
|
}
|
|
|
|
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, errors.Wrapf(err, "failed to convert value %v from event attribute to float64", filteredValue)
|
|
}
|
|
|
|
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, errors.Wrapf(err, "failed to convert value %v from event attribute to int64", filteredValue)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|