libs/pubsub/query: add EXISTS operator (#4077)

## Issue:

This PR adds an "EXISTS" condition to the event query grammar. It enables querying for the occurrence of an event without having to provide a condition for one of its attributes.

As an example, someone interested in all slashing events might currently catch them with a query such as slash.power > 0.

With this PR the event can be captured with slash.power EXISTS or just slash EXISTS to catch by event type.

## Examples:

`slash EXISTS`

## Commits:

* Add EXISTS condition to query grammar

* Gofmt files

* Move PEG instructions out of auto-generated file to prevent overwrite

* Update libs/pubsub/query/query.go

Co-Authored-By: Anton Kaliaev <anton.kalyaev@gmail.com>

* Update changelog and add test case

* Merge with other changes in PR #4070

* Add EXISTS to Conditions() func

* Apply gofmt

* Addressing PR comments
This commit is contained in:
Henrik Aasted Sørensen
2019-11-07 08:30:50 +01:00
committed by Anton Kaliaev
parent 3495a915cc
commit 98c595312a
8 changed files with 947 additions and 547 deletions

View File

@@ -27,6 +27,7 @@ program](https://hackerone.com/tendermint).
- [libs/pubsub] [\#4070](https://github.com/tendermint/tendermint/pull/4070) No longer panic in `Query#(Matches|Conditions)` preferring to return an error instead.
- [libs/pubsub] [\#4070](https://github.com/tendermint/tendermint/pull/4070) Strip out non-numeric characters when attempting to match numeric values.
- [p2p] [\#3991](https://github.com/tendermint/tendermint/issues/3991) Log "has been established or dialed" as debug log instead of Error for connected peers (@whunmr)
- [rpc] [\#4077](https://github.com/tendermint/tendermint/pull/4077) Added support for `EXISTS` clause to the Websocket query interface.
### BUG FIXES:

View File

@@ -77,6 +77,11 @@ func TestParser(t *testing.T) {
{"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},
}

4
libs/pubsub/query/peg.go Normal file
View File

@@ -0,0 +1,4 @@
// nolint
package query
//go:generate peg -inline -switch query.peg

View File

@@ -80,6 +80,8 @@ const (
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 (
@@ -100,8 +102,8 @@ func (q *Query) Conditions() ([]Condition, error) {
conditions := make([]Condition, 0)
buffer, begin, end := q.parser.Buffer, 0, 0
// tokens must be in the following order: event attribute ("tx.gas") -> operator ("=") -> operand ("7")
for _, token := range q.parser.Tokens() {
// 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)
@@ -127,6 +129,10 @@ func (q *Query) Conditions() ([]Condition, error) {
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]
@@ -207,8 +213,9 @@ func (q *Query) Matches(events map[string][]string) (bool, error) {
buffer, begin, end := q.parser.Buffer, 0, 0
// tokens must be in the following order:
// event attribute ("tx.gas") -> operator ("=") -> operand ("7")
for _, token := range q.parser.Tokens() {
// 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)
@@ -233,6 +240,28 @@ func (q *Query) Matches(events map[string][]string) (bool, error) {
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")

View File

@@ -11,6 +11,7 @@ condition <- tag ' '* (le ' '* (number / time / date)
/ g ' '* (number / time / date)
/ equal ' '* (number / time / date / value)
/ contains ' '* value
/ exists
)
tag <- < (![ \t\n\r\\()"'=><] .)+ >
@@ -27,6 +28,7 @@ and <- "AND"
equal <- "="
contains <- "CONTAINS"
exists <- "EXISTS"
le <- "<="
ge <- ">="
l <- "<"

File diff suppressed because it is too large Load Diff

View File

@@ -112,6 +112,44 @@ func TestMatches(t *testing.T) {
false,
false,
},
{"slash EXISTS",
map[string][]string{"slash.reason": {"missing_signature"}, "slash.power": {"6000"}},
false,
true,
false,
},
{"sl EXISTS",
map[string][]string{"slash.reason": {"missing_signature"}, "slash.power": {"6000"}},
false,
true,
false,
},
{"slash EXISTS",
map[string][]string{"transfer.recipient": {"cosmos1gu6y2a0ffteesyeyeesk23082c6998xyzmt9mz"},
"transfer.sender": {"cosmos1crje20aj4gxdtyct7z3knxqry2jqt2fuaey6u5"}},
false,
false,
false,
},
{"slash.reason EXISTS AND slash.power > 1000",
map[string][]string{"slash.reason": {"missing_signature"}, "slash.power": {"6000"}},
false,
true,
false,
},
{"slash.reason EXISTS AND slash.power > 1000",
map[string][]string{"slash.reason": {"missing_signature"}, "slash.power": {"500"}},
false,
false,
false,
},
{"slash.reason EXISTS",
map[string][]string{"transfer.recipient": {"cosmos1gu6y2a0ffteesyeyeesk23082c6998xyzmt9mz"},
"transfer.sender": {"cosmos1crje20aj4gxdtyct7z3knxqry2jqt2fuaey6u5"}},
false,
false,
false,
},
}
for _, tc := range testCases {
@@ -119,7 +157,6 @@ func TestMatches(t *testing.T) {
if !tc.err {
require.Nil(t, err)
}
require.NotNil(t, q, "Query '%s' should not be nil", tc.s)
if tc.matches {
@@ -166,6 +203,12 @@ func TestConditions(t *testing.T) {
{Tag: "tx.time", Op: query.OpGreaterEqual, Operand: txTime},
},
},
{
s: "slashing EXISTS",
conditions: []query.Condition{
{Tag: "slashing", Op: query.OpExists},
},
},
}
for _, tc := range testCases {

View File

@@ -156,8 +156,8 @@ paths:
string, which has a form: "condition AND condition ..." (no OR at the
moment). condition has a form: "key operation operand". key is a string with
a restricted set of possible symbols ( \t\n\r\\()"'=>< are not allowed).
operation can be "=", "<", "<=", ">", ">=", "CONTAINS". operand can be a
string (escaped with single quotes), number, date or time.
operation can be "=", "<", "<=", ">", ">=", "CONTAINS" AND "EXISTS". operand
can be a string (escaped with single quotes), number, date or time.
Examples:
tm.event = 'NewBlock' # new blocks