From 2ebd34288352a2e3b2168b9dd047f7691dfdb8e9 Mon Sep 17 00:00:00 2001 From: Chris Elder Date: Mon, 5 Dec 2016 13:51:56 -0500 Subject: [PATCH] FAB-1172 - Advanced simulation functions for CouchDB Added implementations for the following: QueryExecutor.GetStateRangeScanIterator() QueryExecutor.GetStateMultipleKeys() TxSimulator.DeleteState() TxSimulator.SetStateMultipleKeys() Unit testing was extended for the range query in couchdb.go Additional unit testing for simulation functions will be added following a refactoring of unit tests for goleveldb. Change-Id: Id0907ace75767fe4b296f16c10d18693718d790f Signed-off-by: Chris Elder --- .../couchdbtxmgmt/couchdb_query_executer.go | 41 ++- .../couchdbtxmgmt/couchdb_tx_simulator.go | 49 +++- .../txmgmt/couchdbtxmgmt/couchdb_txmgr.go | 64 ++++- core/ledger/util/couchdb/couchdb.go | 239 +++++++++++++++--- core/ledger/util/couchdb/couchdb_test.go | 110 ++++++++ 5 files changed, 464 insertions(+), 39 deletions(-) diff --git a/core/ledger/kvledger/txmgmt/couchdbtxmgmt/couchdb_query_executer.go b/core/ledger/kvledger/txmgmt/couchdbtxmgmt/couchdb_query_executer.go index 8e83380f9a0..ecae4aff2e0 100644 --- a/core/ledger/kvledger/txmgmt/couchdbtxmgmt/couchdb_query_executer.go +++ b/core/ledger/kvledger/txmgmt/couchdbtxmgmt/couchdb_query_executer.go @@ -39,12 +39,27 @@ func (q *CouchDBQueryExecutor) GetState(ns string, key string) ([]byte, error) { // GetStateMultipleKeys implements method in interface `ledger.QueryExecutor` func (q *CouchDBQueryExecutor) GetStateMultipleKeys(namespace string, keys []string) ([][]byte, error) { - return nil, errors.New("Not yet implemented") + var results [][]byte + var value []byte + var err error + for _, key := range keys { + value, err = q.GetState(namespace, key) + if err != nil { + return nil, err + } + results = append(results, value) + } + return results, nil } // GetStateRangeScanIterator implements method in interface `ledger.QueryExecutor` func (q *CouchDBQueryExecutor) GetStateRangeScanIterator(namespace string, startKey string, endKey string) (ledger.ResultsIterator, error) { - return nil, errors.New("Not yet implemented") + //q.checkDone() + scanner, err := q.txmgr.getCommittedRangeScanner(namespace, startKey, endKey) + if err != nil { + return nil, err + } + return &qKVItr{scanner}, nil } // GetTransactionsForKey - implements method in interface `ledger.QueryExecutor` @@ -61,3 +76,25 @@ func (q *CouchDBQueryExecutor) ExecuteQuery(query string) (ledger.ResultsIterato func (q *CouchDBQueryExecutor) Done() { //TODO - acquire lock when constructing and release the lock here } + +type qKVItr struct { + s *kvScanner +} + +// Next implements Next() method in ledger.ResultsIterator +func (itr *qKVItr) Next() (ledger.QueryResult, error) { + committedKV, err := itr.s.next() + if err != nil { + return nil, err + } + if committedKV == nil { + return nil, nil + } + + return &ledger.KV{Key: committedKV.key, Value: committedKV.value}, nil +} + +// Close implements Close() method in ledger.ResultsIterator +func (itr *qKVItr) Close() { + itr.s.close() +} diff --git a/core/ledger/kvledger/txmgmt/couchdbtxmgmt/couchdb_tx_simulator.go b/core/ledger/kvledger/txmgmt/couchdbtxmgmt/couchdb_tx_simulator.go index 2a24ffde1d5..53578b1d4be 100644 --- a/core/ledger/kvledger/txmgmt/couchdbtxmgmt/couchdb_tx_simulator.go +++ b/core/ledger/kvledger/txmgmt/couchdbtxmgmt/couchdb_tx_simulator.go @@ -20,6 +20,7 @@ import ( "errors" "reflect" + "github.com/hyperledger/fabric/core/ledger" "github.com/hyperledger/fabric/core/ledger/kvledger/txmgmt" logging "github.com/op/go-logging" ) @@ -108,6 +109,16 @@ func (s *CouchDBTxSimulator) GetState(ns string, key string) ([]byte, error) { return value, nil } +// GetStateRangeScanIterator implements method in interface `ledger.QueryExecutor` +func (s *CouchDBTxSimulator) GetStateRangeScanIterator(namespace string, startKey string, endKey string) (ledger.ResultsIterator, error) { + //s.checkDone() + scanner, err := s.txmgr.getCommittedRangeScanner(namespace, startKey, endKey) + if err != nil { + return nil, err + } + return &sKVItr{scanner, s}, nil +} + // SetState implements method in interface `ledger.TxSimulator` func (s *CouchDBTxSimulator) SetState(ns string, key string, value []byte) error { logger.Debugf("===COUCHDB=== Entering CouchDBTxSimulator.SetState()") @@ -190,7 +201,12 @@ func (s *CouchDBTxSimulator) GetTxSimulationResults() ([]byte, error) { // SetStateMultipleKeys implements method in interface `ledger.TxSimulator` func (s *CouchDBTxSimulator) SetStateMultipleKeys(namespace string, kvs map[string][]byte) error { - return errors.New("Not yet implemented") + for k, v := range kvs { + if err := s.SetState(namespace, k, v); err != nil { + return err + } + } + return nil } // CopyState implements method in interface `ledger.TxSimulator` @@ -202,3 +218,34 @@ func (s *CouchDBTxSimulator) CopyState(sourceNamespace string, targetNamespace s func (s *CouchDBTxSimulator) ExecuteUpdate(query string) error { return errors.New("Not supported by KV data model") } + +type sKVItr struct { + scanner *kvScanner + simulator *CouchDBTxSimulator +} + +// Next implements Next() method in ledger.ResultsIterator +// Returns the next item in the result set. The `QueryResult` is expected to be nil when +// the iterator gets exhausted +func (itr *sKVItr) Next() (ledger.QueryResult, error) { + committedKV, err := itr.scanner.next() + if err != nil { + return nil, err + } + if committedKV == nil { + return nil, nil + } + + // Get existing cache for RW at the namespace of the result set if it exists. If none exists, then create it. + nsRWs := itr.simulator.getOrCreateNsRWHolder(itr.scanner.namespace) + nsRWs.readMap[committedKV.key] = &kvReadCache{ + &txmgmt.KVRead{Key: committedKV.key, Version: committedKV.version}, committedKV.value} + + return &ledger.KV{Key: committedKV.key, Value: committedKV.value}, nil +} + +// Close implements Close() method in ledger.ResultsIterator +// which releases resources occupied by the iterator. +func (itr *sKVItr) Close() { + itr.scanner.close() +} diff --git a/core/ledger/kvledger/txmgmt/couchdbtxmgmt/couchdb_txmgr.go b/core/ledger/kvledger/txmgmt/couchdbtxmgmt/couchdb_txmgr.go index 6d7149ba1cf..95d3661c42c 100644 --- a/core/ledger/kvledger/txmgmt/couchdbtxmgmt/couchdb_txmgr.go +++ b/core/ledger/kvledger/txmgmt/couchdbtxmgmt/couchdb_txmgr.go @@ -17,6 +17,7 @@ limitations under the License. package couchdbtxmgmt import ( + "bytes" "encoding/json" "errors" "sync" @@ -36,6 +37,8 @@ import ( var logger = logging.MustGetLogger("couchdbtxmgmt") +var compositeKeySep = []byte{0x00} + // Conf - configuration for `CouchDBTxMgr` type Conf struct { DBPath string @@ -383,6 +386,22 @@ func (txmgr *CouchDBTxMgr) getCommittedValueAndVersion(ns string, key string) ([ return docBytes, ver, nil } +//getCommittedRangeScanner contructs composite start and end keys based on the namespace then calls the CouchDB range scanner +func (txmgr *CouchDBTxMgr) getCommittedRangeScanner(namespace string, startKey string, endKey string) (*kvScanner, error) { + var compositeStartKey []byte + var compositeEndKey []byte + if startKey != "" { + compositeStartKey = constructCompositeKey(namespace, startKey) + } + if endKey != "" { + compositeEndKey = constructCompositeKey(namespace, endKey) + } + + queryResult, _ := txmgr.couchDB.ReadDocRange(string(compositeStartKey), string(compositeEndKey), 1000, 0) + + return newKVScanner(namespace, *queryResult), nil +} + func encodeValue(value []byte, version uint64) []byte { versionBytes := proto.EncodeVarint(version) deleteMarker := 0 @@ -409,7 +428,50 @@ func decodeValue(encodedValue []byte) ([]byte, uint64) { func constructCompositeKey(ns string, key string) []byte { compositeKey := []byte(ns) - compositeKey = append(compositeKey, byte(0)) + compositeKey = append(compositeKey, compositeKeySep...) compositeKey = append(compositeKey, []byte(key)...) return compositeKey } + +func splitCompositeKey(compositeKey []byte) (string, string) { + split := bytes.SplitN(compositeKey, compositeKeySep, 2) + return string(split[0]), string(split[1]) +} + +type kvScanner struct { + cursor int + namespace string + results []couchdb.QueryResult +} + +type committedKV struct { + key string + version *version.Height + value []byte +} + +func newKVScanner(namespace string, queryResults []couchdb.QueryResult) *kvScanner { + return &kvScanner{-1, namespace, queryResults} +} + +func (scanner *kvScanner) next() (*committedKV, error) { + + scanner.cursor++ + + if scanner.cursor >= len(scanner.results) { + return nil, nil + } + + selectedValue := scanner.results[scanner.cursor] + + _, key := splitCompositeKey([]byte(selectedValue.ID)) + + //TODO - change hardcoded version when version support is available in CouchDB + return &committedKV{key, version.NewHeight(1, 1), selectedValue.Value}, nil + +} + +func (scanner *kvScanner) close() { + + scanner = nil +} diff --git a/core/ledger/util/couchdb/couchdb.go b/core/ledger/util/couchdb/couchdb.go index 4fb1e54e51a..5f546050015 100644 --- a/core/ledger/util/couchdb/couchdb.go +++ b/core/ledger/util/couchdb/couchdb.go @@ -27,11 +27,14 @@ import ( "mime" "mime/multipart" "net/http" + "net/http/httputil" "net/textproto" "net/url" "regexp" + "strconv" "strings" + "github.com/hyperledger/fabric/core/ledger/kvledger/version" logging "github.com/op/go-logging" ) @@ -66,7 +69,35 @@ type DBInfo struct { InstanceStartTime string `json:"instance_start_time"` } -// CouchDBConnectionDef contains parameters +//QueryResponse is used for processing REST query responses from CouchDB +type QueryResponse struct { + TotalRows int `json:"total_rows"` + Offset int `json:"offset"` + Rows []struct { + ID string `json:"id"` + Key string `json:"key"` + Value struct { + Rev string `json:"rev"` + } `json:"value"` + Doc json.RawMessage `json:"doc"` + } `json:"rows"` +} + +//Doc is used for capturing if attachments are return in the query from CouchDB +type Doc struct { + ID string `json:"_id"` + Rev string `json:"_rev"` + Attachments json.RawMessage `json:"_attachments"` +} + +//QueryResult is used for returning query results from CouchDB +type QueryResult struct { + ID string + Version *version.Height + Value []byte +} + +//CouchDBConnectionDef contains parameters type CouchDBConnectionDef struct { URL string Username string @@ -107,10 +138,16 @@ func CreateConnectionDefinition(couchDBAddress string, databaseName, username, p logger.Debugf("===COUCHDB=== Entering CreateConnectionDefinition()") - connectURL := fmt.Sprintf("%s//%s", "http:", couchDBAddress) + //connectURL := fmt.Sprintf("%s//%s", "http:", couchDBAddress) + //connectURL := couchDBAddress + + connectURL := &url.URL{ + Host: couchDBAddress, + Scheme: "http", + } //parse the constructed URL to verify no errors - finalURL, err := url.Parse(connectURL) + finalURL, err := url.Parse(connectURL.String()) if err != nil { logger.Errorf("===COUCHDB=== URL parse error: %s", err.Error()) return nil, err @@ -140,11 +177,15 @@ func (dbclient *CouchDBConnectionDef) CreateDatabaseIfNotExist() (*DBOperationRe logger.Debugf("===COUCHDB=== Database %s does not exist.", dbclient.Database) - //create a URL for creating a database - url := fmt.Sprintf("%s/%s", dbclient.URL, dbclient.Database) + connectURL, err := url.Parse(dbclient.URL) + if err != nil { + logger.Errorf("===COUCHDB=== URL parse error: %s", err.Error()) + return nil, err + } + connectURL.Path = dbclient.Database //process the URL with a PUT, creates the database - resp, _, err := dbclient.handleRequest(http.MethodPut, url, nil, "", "") + resp, _, err := dbclient.handleRequest(http.MethodPut, connectURL.String(), nil, "", "") if err != nil { return nil, err } @@ -175,9 +216,14 @@ func (dbclient *CouchDBConnectionDef) CreateDatabaseIfNotExist() (*DBOperationRe //GetDatabaseInfo method provides function to retrieve database information func (dbclient *CouchDBConnectionDef) GetDatabaseInfo() (*DBInfo, *DBReturn, error) { - url := fmt.Sprintf("%s/%s", dbclient.URL, dbclient.Database) + connectURL, err := url.Parse(dbclient.URL) + if err != nil { + logger.Errorf("===COUCHDB=== URL parse error: %s", err.Error()) + return nil, nil, err + } + connectURL.Path = dbclient.Database - resp, couchDBReturn, err := dbclient.handleRequest(http.MethodGet, url, nil, "", "") + resp, couchDBReturn, err := dbclient.handleRequest(http.MethodGet, connectURL.String(), nil, "", "") if err != nil { return nil, couchDBReturn, err } @@ -203,9 +249,14 @@ func (dbclient *CouchDBConnectionDef) DropDatabase() (*DBOperationResponse, erro logger.Debugf("===COUCHDB=== Entering DropDatabase()") - url := fmt.Sprintf("%s/%s", dbclient.URL, dbclient.Database) + connectURL, err := url.Parse(dbclient.URL) + if err != nil { + logger.Errorf("===COUCHDB=== URL parse error: %s", err.Error()) + return nil, err + } + connectURL.Path = dbclient.Database - resp, _, err := dbclient.handleRequest(http.MethodDelete, url, nil, "", "") + resp, _, err := dbclient.handleRequest(http.MethodDelete, connectURL.String(), nil, "", "") if err != nil { return nil, err } @@ -267,15 +318,20 @@ func (dbclient *CouchDBConnectionDef) SaveDoc(id string, rev string, bytesDoc [] logger.Debugf("===COUCHDB=== Entering SaveDoc()") - url := fmt.Sprintf("%s/%s/%s", dbclient.URL, dbclient.Database, id) + saveURL, err := url.Parse(dbclient.URL) + if err != nil { + logger.Errorf("===COUCHDB=== URL parse error: %s", err.Error()) + return "", err + } + saveURL.Path = dbclient.Database + "/" + id logger.Debugf("===COUCHDB=== id=%s, value=%s", id, string(bytesDoc)) if rev == "" { //See if the document already exists, we need the rev for save - _, revdoc, err := dbclient.ReadDoc(id) - if err != nil { + _, revdoc, err2 := dbclient.ReadDoc(id) + if err2 != nil { //set the revision to indicate that the document was not found rev = "" } else { @@ -306,9 +362,9 @@ func (dbclient *CouchDBConnectionDef) SaveDoc(id string, rev string, bytesDoc [] } else { //attachments are included, create the multipart definition - multipartData, multipartBoundary, err := createAttachmentPart(*data, attachments, defaultBoundary) - if err != nil { - return "", err + multipartData, multipartBoundary, err3 := createAttachmentPart(*data, attachments, defaultBoundary) + if err3 != nil { + return "", err3 } //Set the data buffer to the data from the create multi-part data @@ -320,7 +376,7 @@ func (dbclient *CouchDBConnectionDef) SaveDoc(id string, rev string, bytesDoc [] } //handle the request for saving the JSON or attachments - resp, _, err := dbclient.handleRequest(http.MethodPut, url, data, rev, defaultBoundary) + resp, _, err := dbclient.handleRequest(http.MethodPut, saveURL.String(), data, rev, defaultBoundary) if err != nil { return "", err } @@ -410,21 +466,31 @@ func (dbclient *CouchDBConnectionDef) ReadDoc(id string) ([]byte, string, error) logger.Debugf("===COUCHDB=== Entering ReadDoc() id=%s", id) - url := fmt.Sprintf("%s/%s/%s?attachments=true", dbclient.URL, dbclient.Database, id) + readURL, err := url.Parse(dbclient.URL) + if err != nil { + logger.Errorf("===COUCHDB=== URL parse error: %s", err.Error()) + return nil, "", err + } + readURL.Path = dbclient.Database + "/" + id + + query := readURL.Query() + query.Add("attachments", "true") + + readURL.RawQuery = query.Encode() - resp, _, err := dbclient.handleRequest(http.MethodGet, url, nil, "", "") + resp, _, err := dbclient.handleRequest(http.MethodGet, readURL.String(), nil, "", "") if err != nil { return nil, "", err } defer resp.Body.Close() - /* - dump, err := httputil.DumpResponse(resp, true) - if err != nil { - log.Fatal(err) + if logger.IsEnabledFor(logging.DEBUG) { + dump, err2 := httputil.DumpResponse(resp, true) + if err2 != nil { + log.Fatal(err2) } - fmt.Printf("%s", dump) - */ + logger.Debugf("%s", dump) + } //Get the media type from the Content-Type header mediaType, params, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) @@ -517,13 +583,116 @@ func (dbclient *CouchDBConnectionDef) ReadDoc(id string) ([]byte, string, error) } +//ReadDocRange method provides function to a range of documents based on the start and end keys +//startKey and endKey can also be empty strings. If startKey and endKey are empty, all documents are returned +//TODO This function provides a limit option to specify the max number of entries. This will +//need to be added to configuration options. Skip will not be used by Fabric since a consistent +//result set is required +func (dbclient *CouchDBConnectionDef) ReadDocRange(startKey, endKey string, limit, skip int) (*[]QueryResult, error) { + + logger.Debugf("===COUCHDB=== Entering ReadDocRange() startKey=%s, endKey=%s", startKey, endKey) + + var results []QueryResult + + rangeURL, err := url.Parse(dbclient.URL) + if err != nil { + logger.Errorf("===COUCHDB=== URL parse error: %s", err.Error()) + return nil, err + } + rangeURL.Path = dbclient.Database + "/_all_docs" + + query := rangeURL.Query() + query.Set("limit", strconv.Itoa(limit)) + query.Add("skip", strconv.Itoa(skip)) + query.Add("include_docs", "true") + + //Append the startKey if provided + if startKey != "" { + query.Add("startkey", strings.Replace(strconv.QuoteToGraphic(startKey), "\\x00", "\\u0000", 1)) + } + + //Append the endKey if provided + if endKey != "" { + query.Add("endkey", strings.Replace(strconv.QuoteToGraphic(endKey), "\\x00", "\\u0000", 1)) + } + + rangeURL.RawQuery = query.Encode() + + resp, _, err := dbclient.handleRequest(http.MethodGet, rangeURL.String(), nil, "", "") + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if logger.IsEnabledFor(logging.DEBUG) { + dump, err2 := httputil.DumpResponse(resp, true) + if err2 != nil { + log.Fatal(err2) + } + logger.Debugf("%s", dump) + } + + //handle as JSON document + jsonResponseRaw, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + var jsonResponse = &QueryResponse{} + err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse) + if err2 != nil { + return nil, err2 + } + + logger.Debugf("Total Rows: %d", jsonResponse.TotalRows) + + for _, row := range jsonResponse.Rows { + + //logger.Debugf("row: %s", value.Doc) + var jsonDoc = &Doc{} + err3 := json.Unmarshal(row.Doc, &jsonDoc) + if err3 != nil { + return nil, err3 + } + + if jsonDoc.Attachments != nil { + + logger.Debugf("===COUCHDB=== Adding binary docment for id: %s", jsonDoc.ID) + + binaryDocument, _, err := dbclient.ReadDoc(jsonDoc.ID) + if err != nil { + return nil, err + } + + var addDocument = &QueryResult{jsonDoc.ID, version.NewHeight(1, 1), binaryDocument} + + results = append(results, *addDocument) + + } else { + + logger.Debugf("===COUCHDB=== Adding json docment for id: %s", jsonDoc.ID) + + var addDocument = &QueryResult{jsonDoc.ID, version.NewHeight(1, 1), row.Doc} + + results = append(results, *addDocument) + + } + + } + + logger.Debugf("===COUCHDB=== Exiting ReadDocRange()") + + return &results, nil + +} + //handleRequest method is a generic http request handler -func (dbclient *CouchDBConnectionDef) handleRequest(method, url string, data io.Reader, rev string, multipartBoundary string) (*http.Response, *DBReturn, error) { +func (dbclient *CouchDBConnectionDef) handleRequest(method, connectURL string, data io.Reader, rev string, multipartBoundary string) (*http.Response, *DBReturn, error) { - logger.Debugf("===COUCHDB=== Entering handleRequest() method=%s url=%s", method, url) + logger.Debugf("===COUCHDB=== Entering handleRequest() method=%s url=%v", method, connectURL) //Create request based on URL for couchdb operation - req, err := http.NewRequest(method, url, data) + req, err := http.NewRequest(method, connectURL, data) if err != nil { return nil, nil, err } @@ -553,6 +722,7 @@ func (dbclient *CouchDBConnectionDef) handleRequest(method, url string, data io. //add content header for GET if method == http.MethodGet { req.Header.Set("Accept", "multipart/related") + req.Header.Set("Accept", "application/json") } //If username and password are set the use basic auth @@ -560,14 +730,13 @@ func (dbclient *CouchDBConnectionDef) handleRequest(method, url string, data io. req.SetBasicAuth(dbclient.Username, dbclient.Password) } - /* - dump, err := httputil.DumpRequestOut(req, true) - if err != nil { - log.Fatal(err) + if logger.IsEnabledFor(logging.DEBUG) { + dump, err2 := httputil.DumpRequestOut(req, true) + if err2 != nil { + log.Fatal(err2) } - - fmt.Printf("%s", dump) - */ + logger.Debugf("%s", dump) + } //Create the http client client := &http.Client{} diff --git a/core/ledger/util/couchdb/couchdb_test.go b/core/ledger/util/couchdb/couchdb_test.go index bddae95c1d8..66c31a1b0d5 100644 --- a/core/ledger/util/couchdb/couchdb_test.go +++ b/core/ledger/util/couchdb/couchdb_test.go @@ -119,6 +119,7 @@ func TestDBCreateDatabaseAndPersist(t *testing.T) { if ledgerconfig.IsCouchDBEnabled() == true { cleanup() + defer cleanup() //create a new connection db, err := CreateConnectionDefinition(connectURL, database, username, password) @@ -325,3 +326,112 @@ func TestDBTestDropDatabaseBadConnection(t *testing.T) { } } + +func TestDBReadDocumentRange(t *testing.T) { + + if ledgerconfig.IsCouchDBEnabled() == true { + + cleanup() + defer cleanup() + + var assetJSON1 = []byte(`{"asset_name":"marble1","color":"blue","size":"35","owner":"jerry"}`) + var assetJSON2 = []byte(`{"asset_name":"marble2","color":"blue","size":"35","owner":"jerry"}`) + var assetJSON3 = []byte(`{"asset_name":"marble3","color":"blue","size":"35","owner":"jerry"}`) + var assetJSON4 = []byte(`{"asset_name":"marble4","color":"blue","size":"35","owner":"jerry"}`) + + var textString1 = []byte("This is a test. iteration 1") + var textString2 = []byte("This is a test. iteration 2") + var textString3 = []byte("This is a test. iteration 3") + var textString4 = []byte("This is a test. iteration 4") + + attachment1 := Attachment{} + attachment1.AttachmentBytes = textString1 + attachment1.ContentType = "text/plain" + attachment1.Name = "valueBytes" + + attachments1 := []Attachment{} + attachments1 = append(attachments1, attachment1) + + attachment2 := Attachment{} + attachment2.AttachmentBytes = textString2 + attachment2.ContentType = "text/plain" + attachment2.Name = "valueBytes" + + attachments2 := []Attachment{} + attachments2 = append(attachments2, attachment2) + + attachment3 := Attachment{} + attachment3.AttachmentBytes = textString3 + attachment3.ContentType = "text/plain" + attachment3.Name = "valueBytes" + + attachments3 := []Attachment{} + attachments3 = append(attachments3, attachment3) + + attachment4 := Attachment{} + attachment4.AttachmentBytes = textString4 + attachment4.ContentType = "text/plain" + attachment4.Name = "valueBytes" + + attachments4 := []Attachment{} + attachments4 = append(attachments4, attachment4) + + //create a new connection + db, err := CreateConnectionDefinition(connectURL, database, username, password) + testutil.AssertNoError(t, err, fmt.Sprintf("Error when trying to create database connection definition")) + + //create a new database + _, errdb := db.CreateDatabaseIfNotExist() + testutil.AssertNoError(t, errdb, fmt.Sprintf("Error when trying to create database")) + + //Retrieve the info for the new database and make sure the name matches + dbResp, _, errdb := db.GetDatabaseInfo() + testutil.AssertNoError(t, errdb, fmt.Sprintf("Error when trying to retrieve database information")) + testutil.AssertEquals(t, dbResp.DbName, database) + + //Save the test document + _, saveerr1 := db.SaveDoc("1", "", assetJSON1, nil) + testutil.AssertNoError(t, saveerr1, fmt.Sprintf("Error when trying to save a document")) + + //Save the test document + _, saveerr2 := db.SaveDoc("2", "", assetJSON2, nil) + testutil.AssertNoError(t, saveerr2, fmt.Sprintf("Error when trying to save a document")) + + //Save the test document + _, saveerr3 := db.SaveDoc("3", "", assetJSON3, nil) + testutil.AssertNoError(t, saveerr3, fmt.Sprintf("Error when trying to save a document")) + + //Save the test document + _, saveerr4 := db.SaveDoc("4", "", assetJSON4, nil) + testutil.AssertNoError(t, saveerr4, fmt.Sprintf("Error when trying to save a document")) + + //Save the test document + _, saveerr5 := db.SaveDoc("11", "", nil, attachments1) + testutil.AssertNoError(t, saveerr5, fmt.Sprintf("Error when trying to save a document")) + + //Save the test document + _, saveerr6 := db.SaveDoc("12", "", nil, attachments2) + testutil.AssertNoError(t, saveerr6, fmt.Sprintf("Error when trying to save a document")) + + //Save the test document + _, saveerr7 := db.SaveDoc("13", "", nil, attachments3) + testutil.AssertNoError(t, saveerr7, fmt.Sprintf("Error when trying to save a document")) + + //Save the test document + _, saveerr8 := db.SaveDoc("5", "", nil, attachments4) + testutil.AssertNoError(t, saveerr8, fmt.Sprintf("Error when trying to save a document")) + + queryResp, _ := db.ReadDocRange("1", "12", 1000, 0) + + //Ensure the query returns 3 documents + testutil.AssertEquals(t, len(*queryResp), 3) + + /* + for item, _ := range *queryResp { + item. + } + */ + + } + +}