Browse Source

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
pull/4118/head
Henrik Aasted Sørensen 5 years ago
committed by Anton Kaliaev
parent
commit
98c595312a
8 changed files with 830 additions and 430 deletions
  1. +1
    -0
      CHANGELOG_PENDING.md
  2. +5
    -0
      libs/pubsub/query/parser_test.go
  3. +4
    -0
      libs/pubsub/query/peg.go
  4. +33
    -4
      libs/pubsub/query/query.go
  5. +2
    -0
      libs/pubsub/query/query.peg
  6. +739
    -423
      libs/pubsub/query/query.peg.go
  7. +44
    -1
      libs/pubsub/query/query_test.go
  8. +2
    -2
      rpc/swagger/swagger.yaml

+ 1
- 0
CHANGELOG_PENDING.md 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) 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. - [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) - [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: ### BUG FIXES:


+ 5
- 0
libs/pubsub/query/parser_test.go View File

@ -77,6 +77,11 @@ func TestParser(t *testing.T) {
{"account.balance >>= 400", false}, {"account.balance >>= 400", false},
{"account.balance=33.22.1", 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'", true},
{"hash=136E18F7E4C348B780CF873A0BF43922E5BAFA63", false}, {"hash=136E18F7E4C348B780CF873A0BF43922E5BAFA63", false},
} }


+ 4
- 0
libs/pubsub/query/peg.go View File

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

+ 33
- 4
libs/pubsub/query/query.go View File

@ -80,6 +80,8 @@ const (
OpEqual OpEqual
// "CONTAINS"; used to check if a string contains a certain sub string. // "CONTAINS"; used to check if a string contains a certain sub string.
OpContains OpContains
// "EXISTS"; used to check if a certain event attribute is present.
OpExists
) )
const ( const (
@ -100,8 +102,8 @@ func (q *Query) Conditions() ([]Condition, error) {
conditions := make([]Condition, 0) conditions := make([]Condition, 0)
buffer, begin, end := q.parser.Buffer, 0, 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 { switch token.pegRule {
case rulePegText: case rulePegText:
begin, end = int(token.begin), int(token.end) begin, end = int(token.begin), int(token.end)
@ -127,6 +129,10 @@ func (q *Query) Conditions() ([]Condition, error) {
case rulecontains: case rulecontains:
op = OpContains op = OpContains
case ruleexists:
op = OpExists
conditions = append(conditions, Condition{eventAttr, op, nil})
case rulevalue: case rulevalue:
// strip single quotes from value (i.e. "'NewBlock'" -> "NewBlock") // strip single quotes from value (i.e. "'NewBlock'" -> "NewBlock")
valueWithoutSingleQuotes := buffer[begin+1 : end-1] 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 buffer, begin, end := q.parser.Buffer, 0, 0
// tokens must be in the following order: // 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 { switch token.pegRule {
case rulePegText: case rulePegText:
begin, end = int(token.begin), int(token.end) begin, end = int(token.begin), int(token.end)
@ -233,6 +240,28 @@ func (q *Query) Matches(events map[string][]string) (bool, error) {
case rulecontains: case rulecontains:
op = OpContains 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: case rulevalue:
// strip single quotes from value (i.e. "'NewBlock'" -> "NewBlock") // strip single quotes from value (i.e. "'NewBlock'" -> "NewBlock")


+ 2
- 0
libs/pubsub/query/query.peg View File

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


+ 739
- 423
libs/pubsub/query/query.peg.go
File diff suppressed because it is too large
View File


+ 44
- 1
libs/pubsub/query/query_test.go View File

@ -112,6 +112,44 @@ func TestMatches(t *testing.T) {
false, false,
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 { for _, tc := range testCases {
@ -119,7 +157,6 @@ func TestMatches(t *testing.T) {
if !tc.err { if !tc.err {
require.Nil(t, err) require.Nil(t, err)
} }
require.NotNil(t, q, "Query '%s' should not be nil", tc.s) require.NotNil(t, q, "Query '%s' should not be nil", tc.s)
if tc.matches { if tc.matches {
@ -166,6 +203,12 @@ func TestConditions(t *testing.T) {
{Tag: "tx.time", Op: query.OpGreaterEqual, Operand: txTime}, {Tag: "tx.time", Op: query.OpGreaterEqual, Operand: txTime},
}, },
}, },
{
s: "slashing EXISTS",
conditions: []query.Condition{
{Tag: "slashing", Op: query.OpExists},
},
},
} }
for _, tc := range testCases { for _, tc := range testCases {


+ 2
- 2
rpc/swagger/swagger.yaml View File

@ -156,8 +156,8 @@ paths:
string, which has a form: "condition AND condition ..." (no OR at the 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 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). 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: Examples:
tm.event = 'NewBlock' # new blocks tm.event = 'NewBlock' # new blocks


Loading…
Cancel
Save