@ -11,11 +11,12 @@ import (
"github.com/pkg/errors"
dbm "github.com/tendermint/tm-db"
cmn "github.com/tendermint/tendermint/libs/common"
"github.com/tendermint/tendermint/libs/pubsub/query"
"github.com/tendermint/tendermint/state/txindex"
"github.com/tendermint/tendermint/types"
dbm "github.com/tendermint/tm-db"
)
const (
@ -163,8 +164,8 @@ func (txi *TxIndex) indexEvents(result *types.TxResult, hash []byte, store dbm.S
// both lower and upper bounds, so we are not performing a full scan. Results
// from querying indexes are then intersected and returned to the caller.
func ( txi * TxIndex ) Search ( q * query . Query ) ( [ ] * types . TxResult , error ) {
var hashes [ ] [ ] byte
var hashesInitialized bool
filteredHashes := make ( map [ string ] [ ] byte )
// get a list of conditions (like "tx.height > 5")
conditions := q . Conditions ( )
@ -193,10 +194,16 @@ func (txi *TxIndex) Search(q *query.Query) ([]*types.TxResult, error) {
for _ , r := range ranges {
if ! hashesInitialized {
h ashes = txi . matchRange ( r , startKey ( r . key ) )
filteredH ashes = txi . matchRange ( r , startKey ( r . key ) , filteredHashes , true )
hashesInitialized = true
// Ignore any remaining conditions if the first condition resulted
// in no matches (assuming implicit AND operand).
if len ( filteredHashes ) == 0 {
break
}
} else {
hashes = intersect ( hashes , txi . matchRange ( r , startKey ( r . key ) ) )
filteredHashes = txi . matchRange ( r , startKey ( r . key ) , filteredHashes , false )
}
}
}
@ -211,21 +218,26 @@ func (txi *TxIndex) Search(q *query.Query) ([]*types.TxResult, error) {
}
if ! hashesInitialized {
h ashes = txi . match ( c , startKeyForCondition ( c , height ) )
filteredH ashes = txi . match ( c , startKeyForCondition ( c , height ) , filteredHashes , true )
hashesInitialized = true
// Ignore any remaining conditions if the first condition resulted
// in no matches (assuming implicit AND operand).
if len ( filteredHashes ) == 0 {
break
}
} else {
hashes = intersect ( hashes , txi . match ( c , startKeyForCondition ( c , height ) ) )
filteredHashes = txi . match ( c , startKeyForCondition ( c , height ) , filteredHashes , false )
}
}
results := make ( [ ] * types . TxResult , len ( hashes ) )
i := 0
for _ , h := range hashes {
results [ i ] , err = txi . Get ( h )
results := make ( [ ] * types . TxResult , 0 , len ( filteredHashes ) )
for _ , h := range filteredHashes {
res , err := txi . Get ( h )
if err != nil {
return nil , errors . Wrapf ( err , "failed to get Tx{%X}" , h )
}
i ++
results = append ( results , res )
}
// sort by height & index by default
@ -353,63 +365,115 @@ func isRangeOperation(op query.Operator) bool {
}
}
func ( txi * TxIndex ) match ( c query . Condition , startKeyBz [ ] byte ) ( hashes [ ] [ ] byte ) {
// match returns all matching txs by hash that meet a given condition and start
// key. An already filtered result (filteredHashes) is provided such that any
// non-intersecting matches are removed.
//
// NOTE: filteredHashes may be empty if no previous condition has matched.
func ( txi * TxIndex ) match ( c query . Condition , startKeyBz [ ] byte , filteredHashes map [ string ] [ ] byte , firstRun bool ) map [ string ] [ ] byte {
// A previous match was attempted but resulted in no matches, so we return
// no matches (assuming AND operand).
if ! firstRun && len ( filteredHashes ) == 0 {
return filteredHashes
}
tmpHashes := make ( map [ string ] [ ] byte )
if c . Op == query . OpEqual {
it := dbm . IteratePrefix ( txi . store , startKeyBz )
defer it . Close ( )
for ; it . Valid ( ) ; it . Next ( ) {
hashes = append ( hashes , it . Value ( ) )
tmpHashes [ string ( it . Value ( ) ) ] = it . Value ( )
}
} else if c . Op == query . OpContains {
// XXX: startKey does not apply here.
// For example, if startKey = "account.owner/an/" and search query = "account.owner CONTAINS an"
// we can't iterate with prefix "account.owner/an/" because we might miss keys like "account.owner/Ulan/"
it := dbm . IteratePrefix ( txi . store , startKey ( c . Tag ) )
defer it . Close ( )
for ; it . Valid ( ) ; it . Next ( ) {
if ! isTagKey ( it . Key ( ) ) {
continue
}
if strings . Contains ( extractValueFromKey ( it . Key ( ) ) , c . Operand . ( string ) ) {
hashes = append ( hashes , it . Value ( ) )
tmpHashes [ string ( it . Value ( ) ) ] = it . Value ( )
}
}
} else {
panic ( "other operators should be handled already" )
}
return
if len ( tmpHashes ) == 0 || firstRun {
// Either:
//
// 1. Regardless if a previous match was attempted, which may have had
// results, but no match was found for the current condition, then we
// return no matches (assuming AND operand).
//
// 2. A previous match was not attempted, so we return all results.
return tmpHashes
}
// Remove/reduce matches in filteredHashes that were not found in this
// match (tmpHashes).
for k := range filteredHashes {
if tmpHashes [ k ] == nil {
delete ( filteredHashes , k )
}
}
return filteredHashes
}
func ( txi * TxIndex ) matchRange ( r queryRange , startKey [ ] byte ) ( hashes [ ] [ ] byte ) {
// create a map to prevent duplicates
hashesMap := make ( map [ string ] [ ] byte )
// matchRange returns all matching txs by hash that meet a given queryRange and
// start key. An already filtered result (filteredHashes) is provided such that
// any non-intersecting matches are removed.
//
// NOTE: filteredHashes may be empty if no previous condition has matched.
func ( txi * TxIndex ) matchRange ( r queryRange , startKey [ ] byte , filteredHashes map [ string ] [ ] byte , firstRun bool ) map [ string ] [ ] byte {
// A previous match was attempted but resulted in no matches, so we return
// no matches (assuming AND operand).
if ! firstRun && len ( filteredHashes ) == 0 {
return filteredHashes
}
tmpHashes := make ( map [ string ] [ ] byte )
lowerBound := r . lowerBoundValue ( )
upperBound := r . upperBoundValue ( )
it := dbm . IteratePrefix ( txi . store , startKey )
defer it . Close ( )
LOOP :
for ; it . Valid ( ) ; it . Next ( ) {
if ! isTagKey ( it . Key ( ) ) {
continue
}
switch r . AnyBound ( ) . ( type ) {
case int64 :
v , err := strconv . ParseInt ( extractValueFromKey ( it . Key ( ) ) , 10 , 64 )
if err != nil {
continue LOOP
}
include := true
if lowerBound != nil && v < lowerBound . ( int64 ) {
include = false
}
if upperBound != nil && v > upperBound . ( int64 ) {
include = false
}
if include {
hashesMap [ fmt . Sprintf ( "%X" , it . Value ( ) ) ] = it . Value ( )
tmpHashes [ string ( it . Value ( ) ) ] = it . Value ( )
}
// XXX: passing time in a ABCI Tags is not yet implemented
// case time.Time:
// v := strconv.ParseInt(extractValueFromKey(it.Key()), 10, 64)
@ -418,13 +482,27 @@ LOOP:
// }
}
}
hashes = make ( [ ] [ ] byte , len ( hashesMap ) )
i := 0
for _ , h := range hashesMap {
hashes [ i ] = h
i ++
if len ( tmpHashes ) == 0 || firstRun {
// Either:
//
// 1. Regardless if a previous match was attempted, which may have had
// results, but no match was found for the current condition, then we
// return no matches (assuming AND operand).
//
// 2. A previous match was not attempted, so we return all results.
return tmpHashes
}
return
// Remove/reduce matches in filteredHashes that were not found in this
// match (tmpHashes).
for k := range filteredHashes {
if tmpHashes [ k ] == nil {
delete ( filteredHashes , k )
}
}
return filteredHashes
}
///////////////////////////////////////////////////////////////////////////////
@ -471,18 +549,3 @@ func startKey(fields ...interface{}) []byte {
}
return b . Bytes ( )
}
///////////////////////////////////////////////////////////////////////////////
// Utils
func intersect ( as , bs [ ] [ ] byte ) [ ] [ ] byte {
i := make ( [ ] [ ] byte , 0 , cmn . MinInt ( len ( as ) , len ( bs ) ) )
for _ , a := range as {
for _ , b := range bs {
if bytes . Equal ( a , b ) {
i = append ( i , a )
}
}
}
return i
}