Skip to content

Range Queries and Iterators

Overview

The ChaincodeStubInterface exposes several methods that allow smart contract developers to retrieve a sequence of entries. This capability is quite useful as it allows, for instance, the iteration of a range of keys that match a certain criteria or retrieval of the historical changes made to the value associated to a key.

The implementation of this capability relies on iterators. These rely upon the already described method for issuing requests to the peer but may involve multiple requests made to optimise the bandwidth and the consumption of network resources.

Implementation

Three different types of iterators are defined:

  • CommonIteratorInterface: this interface defines the common operations of all types of iterators.
  • StateQueryIteratorInterface: this interface specialises the base iterator interface to iterate over a collection of keys in the ledger.
  • HistoryQueryIteratorInterface: this interface specialises the base iterator interface to iterate over the history of changes of a key.

The listing below shows the definition of the iterator interfaces and the following table maps the ChaincodeStubInterface methods to the corresponding iterators that are returned by such methods.

type CommonIteratorInterface interface {
   HasNext() bool
   Close() error
}

type  {
    CommonIteratorInterface
    Next() (*queryresult.KV, error)
}

type HistoryQueryIteratorInterface {
    CommonIteratorInterface
    Next() (*queryresult.KyModification, error)
}
ChaincodeStubInterface Method Returned Iterator
GetStateByRange StateQueryIteratorInterface
GetStateByRangeWithPagination StateQueryIteratorInterface
GetStateByPartialCompositeKey StateQueryIteratorInterface
GetStateByPartialCompositeKeyWithPagination StateQueryIteratorInterface
GetQueryResult StateQueryIteratorInterface
GetQueryResultWithPagination StateQueryIteratorInterface
GetPrivateDataByRange StateQueryIteratorInterface
GetPrivateDataByPartialCompositeKey StateQueryIteratorInterface
GetPrivateDataQueryResult StateQueryIteratorInterface
GetHistoryForKey HistoryQueryIteratorInterface

Most of the method use the StateQueryIteratorInterface as they provide different options to iterate over a collection of key in the world state (public and private). Only one method returns implementation of HistoryQueryIteratorInterface. These specialised interfaces of the CommonIteratorInterface only differ in the declaration of the return type of the Next() method. This is also reflected in the implementation of these interfaces that are defined in the stub.go file:

  • CommonQueryIterator: implements all the common logic of the iterator and the interaction with the handler.
  • StateQueryIterator: implements the Next() method to cast the returned struct to the *queryresult.KV type.
  • HistoryQueryIterator: implements the Next() method to cast the returned struct to the *queryresult.KeyModification type.

The coordination of these three component with the ChaincodeStub provides support fo all the functions that return iterators.

The ChaincodeStub has the following responsibilities:

  • prepare the information about the range to be queried, by adapting the parameters passed to the different methods that return iterators;
  • invoke the corresponding Handler method to retrieve the first batch of data; and
  • return the appropriate iterator implementation configured with the data that has been retrieved.

The iteration control logic is all implemented in the method that are bound to the CommonIterator struct, while the other two struct simply type casting. The figure below provides an overview of the three structs and how the method bounds to the different types relate to each other.

Design and Implementation of Iterators
Design and Implementation of Iterators

Interaction Flow

The interaction flow with iterators starts from the ChaincodeStub, whose primaru responsibility is interfacing with the Chaincode implementation and expose a rich set of functions to query various elements in the ledger. From an implementation perspective all these functions are supported by three key methods:

  • ChaincodeStub.handleGetStateByRange(....)
  • ChaincodeStub.handleGetQueryResult(....)
  • ChaincodeStub.createQueryMetadata(....)

Moreover, a set of utility methods support the management of key ranges and are used to prepare the information required by these methods.

Iterating Over State Keys

The method ChaincodeStub.handleGetStateByRange(....) is the common denominator for all those methods that retrieves a collection of keys from the ledger. These are:

  • ChaincodeStubInterface.GetStateByRange(...)
  • ChaincodeStubInterface.GetStateByRangeWithPagination(...)
  • ChaincodeStubInterface.GetStateByPartialCompositeKey(...)
  • ChaincodeStubInterface.GetStateByPartialCompositeKeyWithPagination(...)
  • ChaincodeStubInterface.GetPrivateDataByRange(...)
  • ChaincodeStubInterface.GetPrivateDataByPartialCompositeKey(...)

This method invokes the corresponding function in the handler. This in turn sends a ChaincodeMessage of type GET_STATE_BY_RANGE configured with the required parameters to instruct the peer what to query. The listing below shows the implementation of the method.

func (s *ChaincodeStub) handleGetStateByRange(
    collection, startKey, endKey string,
    metadata []byte
) (StateQueryIteratorInterface, *pb.QueryResponseMetadata, error) {

   response, err := s.handler.handleGetStateByRange(collection, startKey, endKey, metadata, s.ChannelID, s.TxID)
   if err != nil {
      return nil, nil, err
   }
   iterator := s.createStateQueryIterator(response)

   responseMetadata, err := createQueryResponseMetadata(response.Metadata)
   if err != nil {
      return nil, nil, err
   }
   return iterator, responseMetadata, nil
}

The most of the execution of the logic is contained in the handler. The stub is responsible for creating the appropriate iterator and deserialising the QueryResponseMetadata which provides information about the records that have been fetched and the bookmark for the search.

Executing Complex Queries

The execution of complex queries is implemented through ChaincodeStub.handleGetQueryResult(...) which supports the functionalities required by the following methods:

  • ChaincodeStubInterface.GetQueryResult(...)
  • ChaincodeStubInterface.GetQueryResultWithPagination(...)
  • GetPrivateDataQueryResult(...)

The implementation invokes the corresponding function in the handler. This in turn sends a ChaincodeMessage of type GET_QUERY_RESULT to the peer configured with the repquired parameter to instruct the peer on what to query. The implementation of this method is essentially the same as the one that support queries by range.

func (s *ChaincodeStub) handleGetQueryResult(
    collection, query string,
    metadata []byte
) (StateQueryIteratorInterface, *pb.QueryResponseMetadata, error) {

   response, err := s.handler.handleGetQueryResult(collection, query, metadata, s.ChannelID, s.TxID)
   if err != nil {
      return nil, nil, err
   }
   iterator := s.createStateQueryIterator(response)

   responseMetadata, err := createQueryResponseMetadata(response.Metadata)
   if err != nil {
      return nil, nil, err
   }
   return iterator, responseMetadata, nil
}

Retrieving the History of a Key

The method ChaincodeStub.GetHistoryForKey(...) provides access to the history of changes of a specific key in the ledger. The ChaincodeStubInterface exposes only one version of this method and therefore the coordination logic for retrieval is implemented directly in this method. In this case, there is no query metadata to process because there is no explicit pagination support as in the other two cases.

func (s *ChaincodeStub) GetHistoryForKey(key string) (StateQueryIteratorInterface, error) {

   response, err := s.handler.handleGetHistoryForKey(key, s.ChannelID, s.TxID)

   if err != nil {
      return nil, err
   }

   return &HistoryQueryIterator{CommonIterator: &CommonIterator{s.handler, s.ChannelID, s.TxID, response, 0}}, nil
}

This method invokes the corresponding method in the handler, which in turn sends a message GET_HISTORY_FOR_KEY to the peer to retrieve the history associated to the specified key.

Pagination Control

The figure below provides an overview of the iterator in action within the context of the chaincode process. The figure shows the case for querying the ledger state by range, and therefore the execution of ChaincodeStub.handleGetStateByRange(...) but the flow is essentially the same for all the other methods that return iterators.

Pagination Control with Iterators
Pagination Control with Iterators

A normal iteration cycle includes the following:

  • invoking HasNext() to verify whether there are more elements to process;
  • in case there are invoking Next() to retrieve the next element; and
  • in case there aren't, close the iterator by invoking Close().

The first two operation can be repeated in sequence until the caller is satisfied with the value that have been returned of the iterator reaches the end of the range.

Internally, the iterator implementation feeds the caller with element from the local batch of data that has been pre-fetched. If the iterator has reached the last element of the local batch, the iterator invokes fetchNextQueryResult(), which in turn calls the method Handler.handleNextQueryStateNext(...) by passing to it the last key that has been retrieved. The handler utilises this information to create a ChaincodeMessage of type QUERY_STATE_NEXT to the peer to retrieve the next batch of items. The call to the handler method is blocking because the implementation uses Handler.sendReceive(....) and therefore it behaves as previously explained.

Similarly, closing an iterator triggers the invocation of Handler.handleQueryStateClose(..) which sends a message of type QU?ERY_STATE_CLOSE to the peer and instruct it release the resources allocated to serve the query.

The pagination control logic is fully implemented in the methods bound to the CommonIterator struct. Even the implementation of the Next() methods is controlled by the common iterator logic defined by CommonIterator.nextResult(...) which has a switch on type to handle the processing of the different types of data. The listing below shows the implementation of the relevant methods of the iterator.

func (iter *CommonIterator) fetchNextQueryResult() error {
    response, err := iter.handler.handleQueryStateNext(iter.response.Id, iter.channelID, iter.txid)
    if err != nil {
        return err
    }
    iter.currentLoc = 0
    iter.response = response
    return nil
}

func (iter *CommonIterator) getResultFromBytes(queryResultBytes *pb.QueryResultBytes, rType resultType) (queryResult, error) {

    if rType == StateQueryResult {
        stateQueryResult := &queryresult.KV{}
        if err := proto.Unmarshal(queryResultBytes.ResultBytes, stateQueryResult); err != nil {
            return nil, fmt.Errorf("error unmarshaling result from bytes: %s", err)
        }
        return stateQueryResult, nil

    } else if rType == HistoryQueryResult {
        historyQueryResult := &queryresult.KeyModification{}
        if err := proto.Unmarshal(queryResultBytes.ResultBytes, historyQueryResult); err != nil {
            return nil, err
        }
        return historyQueryResult, nil
    }
    return nil, errors.New("wrong result type")
}

func (iter *CommonIterator) nextResult(rType resultType) (queryResult, error) {
    if iter.currentLoc < len(iter.response.Results) {
        // On valid access of an element from cached results
        queryResult, err := iter.getResultFromBytes(iter.response.Results[iter.currentLoc], rType)
        if err != nil {
            return nil, err
        }
        iter.currentLoc++

        if iter.currentLoc == len(iter.response.Results) && iter.response.HasMore {
            // On access of last item, pre-fetch to update HasMore flag
            if err = iter.fetchNextQueryResult(); err != nil {
                return nil, err
            }
        }

        return queryResult, err
    } else if !iter.response.HasMore {
        // On call to Next() without check of HasMore
        return nil, errors.New("no such key")
    }

    // should not fall through here
    // case: no cached results but HasMore is true.
    return nil, errors.New("invalid iterator state")
}

The method nextResult(...) is the one that controls the access to the local cache of elements and the iteration logic. It invokes getNextResultFromBytes(...) to deserialise the element to return into the right type, and pre-fetches the next batch of elements by invoking fetchNextQueryResult(...).

Observations

From the implementation of these method every method that provides access to iterators returns iterator implementations that partially retrieve the set of elements that match the searching criteria in the form of data pages. This is the implemented behaviour regardless of whether the smart contract develoeper has called a method ...WithPagination(...).

The question therefore arise on what is the difference between the paginated version and the non-paginated version of the same method. The first set of methods implement explicit pagination while the second set of method rely upon implicit pagination. From an implementation perspective it is always useful to paginate large data sets, especially if there isn't a full consumption of the data in range. Methods that provide explicit pagination provide the smart contract developers with more control on the navigation of the range of keys that are returned by the iterator. For instance, it allows them to skip elements more easily and to start the search from a given page.


Last update: May 1, 2020 03:56:01