mirror of
https://github.com/tendermint/tendermint.git
synced 2026-01-04 04:04:00 +00:00
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:
committed by
Anton Kaliaev
parent
3495a915cc
commit
98c595312a
@@ -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:
|
||||
|
||||
|
||||
@@ -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
4
libs/pubsub/query/peg.go
Normal file
@@ -0,0 +1,4 @@
|
||||
// nolint
|
||||
package query
|
||||
|
||||
//go:generate peg -inline -switch query.peg
|
||||
@@ -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")
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user