Skip to content

Commit ead6705

Browse files
author
Chris Elder
committed
FAB-1818 Create data wrapper for state data in CouchDB
Motivation for this change: Maintain the version inline with the document, without changing the structure/value of the stored document. This will prevent unexpected data values being returned and will prevent possible name collisions with document values. - Create a data wrapper for ledger JSON data stored in CouchDB. - Return the version based on block number and transaction id The wrapper will be implemented as a new key named "data" which will contain the JSON data for the state database. Prior to change example: "doc": { "_id": "2", "_rev": "2-8ee0c31b21ad650e5b872c0b98e59ab5", "version":"1:2" "asset_name": "marble2", "color": "red", "owner": "tom", "size": "25" } Following change: "doc": { "_id": "2", "_rev": "2-8ee0c31b21ad650e5b872c0b98e59ab5", "version":"1:2" "data": { "asset_name": "marble2", "color": "red", "owner": "tom", "size": "25" } } Change-Id: I59391ea926531c46c346fc8448e3d041ca5f3fdf Signed-off-by: Chris Elder <[email protected]>
1 parent cf28448 commit ead6705

File tree

10 files changed

+385
-56
lines changed

10 files changed

+385
-56
lines changed

core/ledger/kvledger/txmgmt/statedb/commontests/test_common.go

+23-6
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,8 @@ func TestBasicRW(t *testing.T, dbProvider statedb.VersionedDBProvider) {
5151
vv, _ := db.GetState("ns1", "key1")
5252
testutil.AssertEquals(t, vv, &vv1)
5353

54-
//TODO re-enable test after Couch version wrapper is added
55-
//vv, _ = db.GetState("ns2", "key4")
56-
//testutil.AssertEquals(t, vv, &vv4)
54+
vv, _ = db.GetState("ns2", "key4")
55+
testutil.AssertEquals(t, vv, &vv4)
5756

5857
sp, err := db.GetLatestSavePoint()
5958
testutil.AssertNoError(t, err, "")
@@ -91,9 +90,8 @@ func TestMultiDBBasicRW(t *testing.T, dbProvider statedb.VersionedDBProvider) {
9190
testutil.AssertNoError(t, err, "")
9291
testutil.AssertEquals(t, sp, savePoint1)
9392

94-
//TODO re-enable test after Couch version wrapper is added
95-
//vv, _ = db2.GetState("ns1", "key1")
96-
//testutil.AssertEquals(t, vv, &vv3)
93+
vv, _ = db2.GetState("ns1", "key1")
94+
testutil.AssertEquals(t, vv, &vv3)
9795

9896
sp, err = db2.GetLatestSavePoint()
9997
testutil.AssertNoError(t, err, "")
@@ -222,4 +220,23 @@ func TestQuery(t *testing.T, dbProvider statedb.VersionedDBProvider) {
222220
queryResult3, err := itr.Next()
223221
testutil.AssertNoError(t, err, "")
224222
testutil.AssertNil(t, queryResult3)
223+
224+
// query with fields
225+
itr, err = db.ExecuteQuery("{\"selector\":{\"owner\":\"jerry\"},\"fields\": [\"owner\", \"asset_name\", \"color\", \"size\"]}")
226+
testutil.AssertNoError(t, err, "")
227+
228+
// verify one jerry result
229+
queryResult1, err = itr.Next()
230+
testutil.AssertNoError(t, err, "")
231+
testutil.AssertNotNil(t, queryResult1)
232+
versionedQueryRecord = queryResult1.(*statedb.VersionedQueryRecord)
233+
stringRecord = string(versionedQueryRecord.Record)
234+
bFoundJerry = strings.Contains(stringRecord, "jerry")
235+
testutil.AssertEquals(t, bFoundJerry, true)
236+
237+
// verify no more results
238+
queryResult2, err = itr.Next()
239+
testutil.AssertNoError(t, err, "")
240+
testutil.AssertNil(t, queryResult2)
241+
225242
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package statecouchdb
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"strings"
7+
)
8+
9+
//ApplyQueryWrapper parses the query string passed to CouchDB
10+
//the wrapper prepends the wrapper "data." to all fields specified in the query
11+
//All fields in the selector must have "data." prepended to the field names
12+
//Fields listed in fields key will have "data." prepended
13+
//Fields in the sort key will have "data." prepended
14+
func ApplyQueryWrapper(queryString string) []byte {
15+
16+
//create a generic map for the query json
17+
jsonQuery := make(map[string]interface{})
18+
19+
//unmarshal the selected json into the generic map
20+
json.Unmarshal([]byte(queryString), &jsonQuery)
21+
22+
//iterate through the JSON query
23+
for jsonKey, jsonValue := range jsonQuery {
24+
25+
//create a case for the data types found in the JSON query
26+
switch jsonQueryPart := jsonValue.(type) {
27+
28+
//if the type is an array, then this is either the "fields" or "sort" part of the query
29+
case []interface{}:
30+
31+
//check to see if this is a "fields" or "sort" array
32+
//if jsonKey == jsonQueryFields || jsonKey == jsonQuerySort {
33+
if jsonKey == jsonQueryFields {
34+
35+
//iterate through the names and add the data wrapper for each field
36+
for itemKey, fieldName := range jsonQueryPart {
37+
38+
//add "data" wrapper to each field definition
39+
jsonQueryPart[itemKey] = fmt.Sprintf("%v.%v", dataWrapper, fieldName)
40+
}
41+
42+
//Add the "_id" and "version" fields, these are needed by default
43+
if jsonKey == jsonQueryFields {
44+
45+
jsonQueryPart = append(jsonQueryPart, "_id")
46+
jsonQueryPart = append(jsonQueryPart, "version")
47+
48+
//Overwrite the query fields if the "_id" field has been added
49+
jsonQuery[jsonQueryFields] = jsonQueryPart
50+
}
51+
52+
}
53+
54+
if jsonKey == jsonQuerySort {
55+
56+
//iterate through the names and add the data wrapper for each field
57+
for sortItemKey, sortField := range jsonQueryPart {
58+
59+
//create a case for the data types found in the JSON query
60+
switch sortFieldType := sortField.(type) {
61+
62+
//if the type is string, then this is a simple array of field names.
63+
//Add the datawrapper to the field name
64+
case string:
65+
66+
//simple case, update the existing array item with the updated name
67+
jsonQueryPart[sortItemKey] = fmt.Sprintf("%v.%v", dataWrapper, sortField)
68+
69+
case interface{}:
70+
71+
//this case is a little more complicated. Here we need to
72+
//iterate over the mapped field names since this is an array of objects
73+
//example: {"fieldname":"desc"}
74+
for key, itemValue := range sortField.(map[string]interface{}) {
75+
//delete the mapping for the field definition, since we have to change the
76+
//value of the key
77+
delete(sortField.(map[string]interface{}), key)
78+
79+
//add the key back into the map with the field name wrapped with then "data" wrapper
80+
sortField.(map[string]interface{})[fmt.Sprintf("%v.%v", dataWrapper, key)] = itemValue
81+
}
82+
83+
default:
84+
85+
logger.Debugf("The type %v was not recognized as a valid sort field type.", sortFieldType)
86+
87+
}
88+
89+
}
90+
}
91+
92+
case interface{}:
93+
94+
//if this is the "selector", the field names need to be mapped with the
95+
//data wrapper
96+
if jsonKey == jsonQuerySelector {
97+
98+
processSelector(jsonQueryPart.(map[string]interface{}))
99+
100+
}
101+
102+
default:
103+
104+
logger.Debugf("The value %v was not recognized as a valid selector field.", jsonKey)
105+
106+
}
107+
}
108+
109+
//Marshal the updated json query
110+
editedQuery, _ := json.Marshal(jsonQuery)
111+
112+
logger.Debugf("Rewritten query with data wrapper: %s", editedQuery)
113+
114+
return editedQuery
115+
116+
}
117+
118+
//processSelector is a recursion function for traversing the selector part of the query
119+
func processSelector(selectorFragment map[string]interface{}) {
120+
121+
//iterate through the top level definitions
122+
for itemKey, itemValue := range selectorFragment {
123+
124+
//check to see if the itemKey starts with a $. If so, this indicates an operator
125+
if strings.HasPrefix(fmt.Sprintf("%s", itemKey), "$") {
126+
127+
processSelector(itemValue.(map[string]interface{}))
128+
129+
} else {
130+
131+
//delete the mapping for the field definition, since we have to change the
132+
//value of the key
133+
delete(selectorFragment, itemKey)
134+
//add the key back into the map with the field name wrapped with then "data" wrapper
135+
selectorFragment[fmt.Sprintf("%v.%v", dataWrapper, itemKey)] = itemValue
136+
137+
}
138+
}
139+
}

core/ledger/kvledger/txmgmt/statedb/statecouchdb/statecouchdb.go

+95-12
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"encoding/json"
2222
"errors"
2323
"fmt"
24+
"strconv"
2425
"strings"
2526
"sync"
2627

@@ -37,6 +38,11 @@ var compositeKeySep = []byte{0x00}
3738
var lastKeyIndicator = byte(0x01)
3839
var savePointKey = []byte{0x00}
3940

41+
var dataWrapper = "data"
42+
var jsonQuerySort = "sort"
43+
var jsonQueryFields = "fields"
44+
var jsonQuerySelector = "selector"
45+
4046
// VersionedDBProvider implements interface VersionedDBProvider
4147
type VersionedDBProvider struct {
4248
couchInstance *couchdb.CouchInstance
@@ -135,9 +141,56 @@ func (vdb *VersionedDB) GetState(namespace string, key string) (*statedb.Version
135141
}
136142
}
137143

138-
ver := version.NewHeight(1, 1) //TODO - version hardcoded to 1 is a temporary value for the prototype
144+
//remove the data wrapper and return the value and version
145+
returnValue, returnVersion := removeDataWrapper(docBytes)
146+
147+
return &statedb.VersionedValue{Value: returnValue, Version: &returnVersion}, nil
148+
}
149+
150+
func removeDataWrapper(wrappedValue []byte) ([]byte, version.Height) {
151+
152+
//initialize the return value
153+
returnValue := []byte{}
154+
155+
//initialize a default return version
156+
returnVersion := version.NewHeight(0, 0)
157+
158+
//if this is a JSON, then remove the data wrapper
159+
if couchdb.IsJSON(string(wrappedValue)) {
160+
161+
//create a generic map for the json
162+
jsonResult := make(map[string]interface{})
163+
164+
//unmarshal the selected json into the generic map
165+
json.Unmarshal(wrappedValue, &jsonResult)
166+
167+
//place the result json in the data key
168+
returnMap := jsonResult[dataWrapper]
169+
170+
//marshal the mapped data. this wrappers the result in a key named "data"
171+
returnValue, _ = json.Marshal(returnMap)
172+
173+
//create an array containing the blockNum and txNum
174+
versionArray := strings.Split(fmt.Sprintf("%s", jsonResult["version"]), ":")
175+
176+
//convert the blockNum from String to unsigned int
177+
blockNum, _ := strconv.ParseUint(versionArray[0], 10, 64)
178+
179+
//convert the txNum from String to unsigned int
180+
txNum, _ := strconv.ParseUint(versionArray[1], 10, 64)
181+
182+
//create the version based on the blockNum and txNum
183+
returnVersion = version.NewHeight(blockNum, txNum)
184+
185+
} else {
186+
187+
//this is a binary, so decode the value and version from the binary
188+
returnValue, returnVersion = statedb.DecodeValue(wrappedValue)
189+
190+
}
191+
192+
return returnValue, *returnVersion
139193

140-
return &statedb.VersionedValue{Value: docBytes, Version: ver}, nil
141194
}
142195

143196
// GetStateMultipleKeys implements method in VersionedDB interface
@@ -181,7 +234,7 @@ func (vdb *VersionedDB) ExecuteQuery(query string) (statedb.ResultsIterator, err
181234
//TODO - limit is currently set at 1000, eventually this will need to be changed
182235
//to reflect a config option and potentially return an exception if the threshold is exceeded
183236
// skip (paging) is not utilized by fabric
184-
queryResult, err := vdb.db.QueryDocuments(query, 1000, 0)
237+
queryResult, err := vdb.db.QueryDocuments(string(ApplyQueryWrapper(query)), 1000, 0)
185238
if err != nil {
186239
logger.Debugf("Error calling QueryDocuments(): %s\n", err.Error())
187240
return nil, err
@@ -219,7 +272,7 @@ func (vdb *VersionedDB) ApplyUpdates(batch *statedb.UpdateBatch, height *version
219272
if couchdb.IsJSON(string(vv.Value)) {
220273

221274
// SaveDoc using couchdb client and use JSON format
222-
rev, err := vdb.db.SaveDoc(string(compositeKey), "", vv.Value, nil)
275+
rev, err := vdb.db.SaveDoc(string(compositeKey), "", addVersionAndChainCodeID(vv.Value, ns, vv.Version), nil)
223276
if err != nil {
224277
logger.Errorf("Error during Commit(): %s\n", err.Error())
225278
return err
@@ -232,23 +285,22 @@ func (vdb *VersionedDB) ApplyUpdates(batch *statedb.UpdateBatch, height *version
232285

233286
//Create an attachment structure and load the bytes
234287
attachment := &couchdb.Attachment{}
235-
attachment.AttachmentBytes = vv.Value
288+
attachment.AttachmentBytes = statedb.EncodeValue(vv.Value, vv.Version)
236289
attachment.ContentType = "application/octet-stream"
237290
attachment.Name = "valueBytes"
238291

239292
attachments := []couchdb.Attachment{}
240293
attachments = append(attachments, *attachment)
241294

242295
// SaveDoc using couchdb client and use attachment to persist the binary data
243-
rev, err := vdb.db.SaveDoc(string(compositeKey), "", nil, attachments)
296+
rev, err := vdb.db.SaveDoc(string(compositeKey), "", addVersionAndChainCodeID(nil, ns, vv.Version), attachments)
244297
if err != nil {
245298
logger.Errorf("Error during Commit(): %s\n", err.Error())
246299
return err
247300
}
248301
if rev != "" {
249302
logger.Debugf("Saved document revision number: %s\n", rev)
250303
}
251-
252304
}
253305
}
254306
}
@@ -263,6 +315,33 @@ func (vdb *VersionedDB) ApplyUpdates(batch *statedb.UpdateBatch, height *version
263315
return nil
264316
}
265317

318+
//addVersionAndChainCodeID adds keys for version and chaincodeID to the JSON value
319+
func addVersionAndChainCodeID(value []byte, chaincodeID string, version *version.Height) []byte {
320+
321+
//create a version mapping
322+
jsonMap := map[string]interface{}{"version": fmt.Sprintf("%v:%v", version.BlockNum, version.TxNum)}
323+
324+
//add the chaincodeID
325+
jsonMap["chaincodeid"] = chaincodeID
326+
327+
//Add the wrapped data if the value is not null
328+
if value != nil {
329+
330+
//create a new genericMap
331+
rawJSON := (*json.RawMessage)(&value)
332+
333+
//add the rawJSON to the map
334+
jsonMap[dataWrapper] = rawJSON
335+
336+
}
337+
338+
//marshal the data to a byte array
339+
returnJSON, _ := json.Marshal(jsonMap)
340+
341+
return returnJSON
342+
343+
}
344+
266345
// Savepoint docid (key) for couchdb
267346
const savepointDocID = "statedb_savepoint"
268347

@@ -379,10 +458,12 @@ func (scanner *kvScanner) Next() (statedb.QueryResult, error) {
379458

380459
_, key := splitCompositeKey([]byte(selectedKV.ID))
381460

382-
//TODO - change hardcoded version (1,1) when version header is available in CouchDB
461+
//remove the data wrapper and return the value and version
462+
returnValue, returnVersion := removeDataWrapper(selectedKV.Value)
463+
383464
return &statedb.VersionedKV{
384465
CompositeKey: statedb.CompositeKey{Namespace: scanner.namespace, Key: key},
385-
VersionedValue: statedb.VersionedValue{Value: selectedKV.Value, Version: version.NewHeight(1, 1)}}, nil
466+
VersionedValue: statedb.VersionedValue{Value: returnValue, Version: &returnVersion}}, nil
386467
}
387468

388469
func (scanner *kvScanner) Close() {
@@ -410,12 +491,14 @@ func (scanner *queryScanner) Next() (statedb.QueryResult, error) {
410491

411492
namespace, key := splitCompositeKey([]byte(selectedResultRecord.ID))
412493

413-
//TODO - change hardcoded version (1,1) when version support is available in CouchDB
494+
//remove the data wrapper and return the value and version
495+
returnValue, returnVersion := removeDataWrapper(selectedResultRecord.Value)
496+
414497
return &statedb.VersionedQueryRecord{
415498
Namespace: namespace,
416499
Key: key,
417-
Version: version.NewHeight(1, 1),
418-
Record: selectedResultRecord.Value}, nil
500+
Version: &returnVersion,
501+
Record: returnValue}, nil
419502
}
420503

421504
func (scanner *queryScanner) Close() {

0 commit comments

Comments
 (0)