Skip to content

Commit

Permalink
FAB-2189 Scope rich queries to chaincode(QueryWrapper)
Browse files Browse the repository at this point in the history
Note:
This change depends on the following items:
https://gerrit.hyperledger.org/r/5729
https://gerrit.hyperledger.org/r/5879

Motivation for this change:
Need to inject chaincodeid filter on rich queries called from chaincode.

- This change will add the QueryWrapper changes needed.

- Add unit tests for query wrapper changes

- Add unit tests for statedb query changes

Change-Id: I18ccd28e5f3cb95141f2f2402af9d5d7bae30b1a
Signed-off-by: Chris Elder <[email protected]>
  • Loading branch information
Chris Elder committed Feb 13, 2017
1 parent 9da35a2 commit 81f439e
Show file tree
Hide file tree
Showing 4 changed files with 341 additions and 96 deletions.
142 changes: 136 additions & 6 deletions core/ledger/kvledger/txmgmt/statedb/commontests/test_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,11 +202,24 @@ func TestQuery(t *testing.T, dbProvider statedb.VersionedDBProvider) {
jsonValue9 := "{\"asset_name\": \"marble9\",\"color\": \"green\",\"size\": 9,\"owner\": \"fred\"}"
batch.Put("ns1", "key9", []byte(jsonValue9), version.NewHeight(1, 9))
jsonValue10 := "{\"asset_name\": \"marble10\",\"color\": \"green\",\"size\": 10,\"owner\": \"mary\"}"
batch.Put("ns1", "key10", []byte(jsonValue10), version.NewHeight(1, 12))
savePoint := version.NewHeight(2, 12)
batch.Put("ns1", "key10", []byte(jsonValue10), version.NewHeight(1, 10))

//add keys for a separate namespace
batch.Put("ns2", "key1", []byte(jsonValue1), version.NewHeight(1, 11))
batch.Put("ns2", "key2", []byte(jsonValue2), version.NewHeight(1, 12))
batch.Put("ns2", "key3", []byte(jsonValue3), version.NewHeight(1, 13))
batch.Put("ns2", "key4", []byte(jsonValue4), version.NewHeight(1, 14))
batch.Put("ns2", "key5", []byte(jsonValue5), version.NewHeight(1, 15))
batch.Put("ns2", "key6", []byte(jsonValue6), version.NewHeight(1, 16))
batch.Put("ns2", "key7", []byte(jsonValue7), version.NewHeight(1, 17))
batch.Put("ns2", "key8", []byte(jsonValue8), version.NewHeight(1, 18))
batch.Put("ns2", "key9", []byte(jsonValue9), version.NewHeight(1, 19))
batch.Put("ns2", "key10", []byte(jsonValue10), version.NewHeight(1, 20))

savePoint := version.NewHeight(2, 21)
db.ApplyUpdates(batch, savePoint)

// query for owner=jerry
// query for owner=jerry, use namespace "ns1"
itr, err := db.ExecuteQuery("ns1", "{\"selector\":{\"owner\":\"jerry\"}}")
testutil.AssertNoError(t, err, "")

Expand All @@ -224,6 +237,33 @@ func TestQuery(t *testing.T, dbProvider statedb.VersionedDBProvider) {
testutil.AssertNoError(t, err, "")
testutil.AssertNil(t, queryResult2)

// query for owner=jerry, use namespace "ns2"
itr, err = db.ExecuteQuery("ns2", "{\"selector\":{\"owner\":\"jerry\"}}")
testutil.AssertNoError(t, err, "")

// verify one jerry result
queryResult1, err = itr.Next()
testutil.AssertNoError(t, err, "")
testutil.AssertNotNil(t, queryResult1)
versionedQueryRecord = queryResult1.(*statedb.VersionedQueryRecord)
stringRecord = string(versionedQueryRecord.Record)
bFoundRecord = strings.Contains(stringRecord, "jerry")
testutil.AssertEquals(t, bFoundRecord, true)

// verify no more results
queryResult2, err = itr.Next()
testutil.AssertNoError(t, err, "")
testutil.AssertNil(t, queryResult2)

// query for owner=jerry, use namespace "ns3"
itr, err = db.ExecuteQuery("ns3", "{\"selector\":{\"owner\":\"jerry\"}}")
testutil.AssertNoError(t, err, "")

// verify results - should be no records
queryResult1, err = itr.Next()
testutil.AssertNoError(t, err, "")
testutil.AssertNil(t, queryResult1)

// query using bad query string
itr, err = db.ExecuteQuery("ns1", "this is an invalid query string")
testutil.AssertError(t, err, "Should have received an error for invalid query string")
Expand All @@ -237,7 +277,7 @@ func TestQuery(t *testing.T, dbProvider statedb.VersionedDBProvider) {
testutil.AssertNoError(t, err, "")
testutil.AssertNil(t, queryResult3)

// query with fields
// query with fields, namespace "ns1"
itr, err = db.ExecuteQuery("ns1", "{\"selector\":{\"owner\":\"jerry\"},\"fields\": [\"owner\", \"asset_name\", \"color\", \"size\"]}")
testutil.AssertNoError(t, err, "")

Expand All @@ -255,7 +295,34 @@ func TestQuery(t *testing.T, dbProvider statedb.VersionedDBProvider) {
testutil.AssertNoError(t, err, "")
testutil.AssertNil(t, queryResult2)

// query with complex selector
// query with fields, namespace "ns2"
itr, err = db.ExecuteQuery("ns2", "{\"selector\":{\"owner\":\"jerry\"},\"fields\": [\"owner\", \"asset_name\", \"color\", \"size\"]}")
testutil.AssertNoError(t, err, "")

// verify one jerry result
queryResult1, err = itr.Next()
testutil.AssertNoError(t, err, "")
testutil.AssertNotNil(t, queryResult1)
versionedQueryRecord = queryResult1.(*statedb.VersionedQueryRecord)
stringRecord = string(versionedQueryRecord.Record)
bFoundRecord = strings.Contains(stringRecord, "jerry")
testutil.AssertEquals(t, bFoundRecord, true)

// verify no more results
queryResult2, err = itr.Next()
testutil.AssertNoError(t, err, "")
testutil.AssertNil(t, queryResult2)

// query with fields, namespace "ns3"
itr, err = db.ExecuteQuery("ns3", "{\"selector\":{\"owner\":\"jerry\"},\"fields\": [\"owner\", \"asset_name\", \"color\", \"size\"]}")
testutil.AssertNoError(t, err, "")

// verify no results
queryResult1, err = itr.Next()
testutil.AssertNoError(t, err, "")
testutil.AssertNil(t, queryResult1)

// query with complex selector, namespace "ns1"
itr, err = db.ExecuteQuery("ns1", "{\"selector\":{\"$and\":[{\"size\":{\"$gt\": 5}},{\"size\":{\"$lt\":8}},{\"$not\":{\"size\":6}}]}}")
testutil.AssertNoError(t, err, "")

Expand All @@ -273,7 +340,34 @@ func TestQuery(t *testing.T, dbProvider statedb.VersionedDBProvider) {
testutil.AssertNoError(t, err, "")
testutil.AssertNil(t, queryResult2)

// query with embedded implicit "AND" and explicit "OR"
// query with complex selector, namespace "ns2"
itr, err = db.ExecuteQuery("ns2", "{\"selector\":{\"$and\":[{\"size\":{\"$gt\": 5}},{\"size\":{\"$lt\":8}},{\"$not\":{\"size\":6}}]}}")
testutil.AssertNoError(t, err, "")

// verify one fred result
queryResult1, err = itr.Next()
testutil.AssertNoError(t, err, "")
testutil.AssertNotNil(t, queryResult1)
versionedQueryRecord = queryResult1.(*statedb.VersionedQueryRecord)
stringRecord = string(versionedQueryRecord.Record)
bFoundRecord = strings.Contains(stringRecord, "fred")
testutil.AssertEquals(t, bFoundRecord, true)

// verify no more results
queryResult2, err = itr.Next()
testutil.AssertNoError(t, err, "")
testutil.AssertNil(t, queryResult2)

// query with complex selector, namespace "ns3"
itr, err = db.ExecuteQuery("ns3", "{\"selector\":{\"$and\":[{\"size\":{\"$gt\": 5}},{\"size\":{\"$lt\":8}},{\"$not\":{\"size\":6}}]}}")
testutil.AssertNoError(t, err, "")

// verify no more results
queryResult1, err = itr.Next()
testutil.AssertNoError(t, err, "")
testutil.AssertNil(t, queryResult1)

// query with embedded implicit "AND" and explicit "OR", namespace "ns1"
itr, err = db.ExecuteQuery("ns1", "{\"selector\":{\"color\":\"green\",\"$or\":[{\"owner\":\"fred\"},{\"owner\":\"mary\"}]}}")
testutil.AssertNoError(t, err, "")

Expand All @@ -300,4 +394,40 @@ func TestQuery(t *testing.T, dbProvider statedb.VersionedDBProvider) {
testutil.AssertNoError(t, err, "")
testutil.AssertNil(t, queryResult3)

// query with embedded implicit "AND" and explicit "OR", namespace "ns2"
itr, err = db.ExecuteQuery("ns2", "{\"selector\":{\"color\":\"green\",\"$or\":[{\"owner\":\"fred\"},{\"owner\":\"mary\"}]}}")
testutil.AssertNoError(t, err, "")

// verify one green result
queryResult1, err = itr.Next()
testutil.AssertNoError(t, err, "")
testutil.AssertNotNil(t, queryResult1)
versionedQueryRecord = queryResult1.(*statedb.VersionedQueryRecord)
stringRecord = string(versionedQueryRecord.Record)
bFoundRecord = strings.Contains(stringRecord, "green")
testutil.AssertEquals(t, bFoundRecord, true)

// verify another green result
queryResult2, err = itr.Next()
testutil.AssertNoError(t, err, "")
testutil.AssertNotNil(t, queryResult2)
versionedQueryRecord = queryResult2.(*statedb.VersionedQueryRecord)
stringRecord = string(versionedQueryRecord.Record)
bFoundRecord = strings.Contains(stringRecord, "green")
testutil.AssertEquals(t, bFoundRecord, true)

// verify no more results
queryResult3, err = itr.Next()
testutil.AssertNoError(t, err, "")
testutil.AssertNil(t, queryResult3)

// query with embedded implicit "AND" and explicit "OR", namespace "ns3"
itr, err = db.ExecuteQuery("ns3", "{\"selector\":{\"color\":\"green\",\"$or\":[{\"owner\":\"fred\"},{\"owner\":\"mary\"}]}}")
testutil.AssertNoError(t, err, "")

// verify no results
queryResult1, err = itr.Next()
testutil.AssertNoError(t, err, "")
testutil.AssertNil(t, queryResult1)

}
110 changes: 88 additions & 22 deletions core/ledger/kvledger/txmgmt/statedb/statecouchdb/query_wrapper.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
/*
Copyright IBM Corp. 2016 All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package statecouchdb

import (
Expand All @@ -8,6 +24,7 @@ import (

var dataWrapper = "data"
var jsonQueryFields = "fields"
var jsonQuerySelector = "selector"

var validOperators = []string{"$and", "$or", "$not", "$nor", "$all", "$elemMatch",
"$lt", "$lte", "$eq", "$ne", "$gte", "$gt", "$exits", "$type", "$in", "$nin",
Expand All @@ -20,6 +37,9 @@ All fields in the selector must have "data." prepended to the field names
Fields listed in fields key will have "data." prepended
Fields in the sort key will have "data." prepended
Also, the query will be scoped to the chaincodeid if the contextID is supplied
In the example a contextID of "marble" is assumed.
Example:
Source Query:
Expand All @@ -28,50 +48,95 @@ Source Query:
"sort": ["size", "color"], "limit": 10, "skip": 0}
Result Wrapped Query:
{"selector":{"data.owner":{"$eq":"tom"}},
{"selector":{"$and":[{"chaincodeid":"marble"},{"data.owner":{"$eq":"tom"}}]},
"fields": ["data.owner","data.asset_name","data.color","data.size","_id","version"],
"sort":["data.size","data.color"],"limit":10,"skip":0}
*/
func ApplyQueryWrapper(namespace, queryString string) string {

//TODO - namespace is being added to support scoping queries to the correct chaincode context
// A followup change will add the implementation for enabling the namespace filter
func ApplyQueryWrapper(namespace, queryString string) (string, error) {

//create a generic map for the query json
jsonQueryMap := make(map[string]interface{})

//unmarshal the selected json into the generic map
json.Unmarshal([]byte(queryString), &jsonQueryMap)
err := json.Unmarshal([]byte(queryString), &jsonQueryMap)
if err != nil {
return "", err
}

//traverse through the json query and wrap any field names
processAndWrapQuery(jsonQueryMap)

//process the query and add the version and fields if fields are specified
for jsonKey, jsonValue := range jsonQueryMap {

//Add the "_id" and "version" fields, these are needed by default
if jsonKey == jsonQueryFields {

//check to see if this is an interface map
if reflect.TypeOf(jsonValue).String() == "[]interface {}" {

//Add the "_id" and "version" fields, these are needed by default
//Overwrite the query fields if the "_id" field has been added
jsonQueryMap[jsonQueryFields] = append(jsonValue.([]interface{}), "_id", "version")
}
//if "fields" are specified in the query, the add the "_id", "version" and "chaincodeid" fields
if jsonValue, ok := jsonQueryMap[jsonQueryFields]; ok {
//check to see if this is an interface map
if reflect.TypeOf(jsonValue).String() == "[]interface {}" {

//Add the "_id" and "version" fields, these are needed by default
//Overwrite the query fields if the "_id" field has been added
jsonQueryMap[jsonQueryFields] = append(jsonValue.([]interface{}),
"_id", "version", "chaincodeid")
}
}

//Check to see if the "selector" is specified in the query
if jsonValue, ok := jsonQueryMap[jsonQuerySelector]; ok {
//if the "selector" is found, then add the "$and" clause and the namespace filter
setNamespaceInSelector(namespace, jsonValue, jsonQueryMap)
} else {
//if the "selector" is not found, then add a default namespace filter
setDefaultNamespaceInSelector(namespace, jsonQueryMap)
}

//Marshal the updated json query
editedQuery, _ := json.Marshal(jsonQueryMap)

logger.Debugf("Rewritten query with data wrapper: %s", editedQuery)

return string(editedQuery)
return string(editedQuery), nil

}

//setNamespaceInSelector adds an additional heirarchy in the "selector"
//{"owner": {"$eq": "tom"}}
//would be mapped as (assuming a namespace of "marble"):
//{"$and":[{"chaincodeid":"marble"},{"data.owner":{"$eq":"tom"}}]}
func setNamespaceInSelector(namespace, jsonValue interface{},
jsonQueryMap map[string]interface{}) {

//create a array to store the parts of the query
var queryParts = make([]interface{}, 0)

//Add the namespace filter to filter on the chaincodeid
namespaceFilter := make(map[string]interface{})
namespaceFilter["chaincodeid"] = namespace

//Add the context filter and the existing selector value
queryParts = append(queryParts, namespaceFilter, jsonValue)

//Create a new mapping for the new query stucture
mappedSelector := make(map[string]interface{})

//Specify the "$and" operator for the parts of the query
mappedSelector["$and"] = queryParts

//Set the new mapped selector to the query selector
jsonQueryMap[jsonQuerySelector] = mappedSelector

}

//setDefaultNamespaceInSelector adds an default namespace filter in "selector"
//If no selector is specified, the following is mapped to the "selector"
//assuming a namespace of "marble"
//{"chaincodeid":"marble"}
func setDefaultNamespaceInSelector(namespace string, jsonQueryMap map[string]interface{}) {

//Add the context filter to filter on the chaincodeid
namespaceFilter := make(map[string]interface{})
namespaceFilter["chaincodeid"] = namespace

//Set the new mapped selector to the query selector
jsonQueryMap[jsonQuerySelector] = namespaceFilter
}

func processAndWrapQuery(jsonQueryMap map[string]interface{}) {
Expand Down Expand Up @@ -125,7 +190,8 @@ func processAndWrapQuery(jsonQueryMap map[string]interface{}) {
}
}

//processInterfaceMap processes an interface map and wraps field names or traverses the next level of the json query
//processInterfaceMap processes an interface map and wraps field names or traverses
//the next level of the json query
func processInterfaceMap(jsonFragment map[string]interface{}) {

//iterate the the item in the map
Expand Down
Loading

0 comments on commit 81f439e

Please sign in to comment.