Skip to content
This repository was archived by the owner on Mar 27, 2024. It is now read-only.

Commit 91dd4c2

Browse files
author
Derek Trider
committed
feat: GetBulk method for new CouchDB storage implementation
- Also includes a minor refactor for the getQueryOptions function that was added in my previous commit. Signed-off-by: Derek Trider <Derek.Trider@securekey.com>
1 parent ab74010 commit 91dd4c2

File tree

4 files changed

+276
-14
lines changed

4 files changed

+276
-14
lines changed

component/newstorage/couchdb/store.go

Lines changed: 112 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,14 @@ import ( //nolint:gci // False positive, seemingly caused by the CouchDB driver
2525
const (
2626
couchDBUsersTable = "_users"
2727
revIDFieldKey = "_rev"
28+
deletedFieldKey = "_deleted"
2829

2930
designDocumentName = "AriesStorageDesignDocument"
3031
payloadFieldKey = "payload"
3132

3233
// Hardcoded strings returned from Kivik/CouchDB that we check for.
3334
docNotFoundErrMsgFromKivik = "Not Found: missing"
35+
bulkGetDocNotFoundErrMsgFromKivik = "not_found: missing"
3436
docDeletedErrMsgFromKivik = "Not Found: deleted"
3537
databaseNotFoundErrMsgFromKivik = "Not Found: Database does not exist."
3638
documentUpdateConflictErrMsgFromKivik = "Conflict: Document update conflict."
@@ -61,6 +63,7 @@ type db interface {
6163
Put(ctx context.Context, docID string, doc interface{}, options ...kivik.Options) (rev string, err error)
6264
Find(ctx context.Context, query interface{}, options ...kivik.Options) (*kivik.Rows, error)
6365
Delete(ctx context.Context, docID, rev string, options ...kivik.Options) (newRev string, err error)
66+
BulkGet(ctx context.Context, docs []kivik.BulkGetReference, options ...kivik.Options) (*kivik.Rows, error)
6467
Close(ctx context.Context) error
6568
}
6669

@@ -339,7 +342,7 @@ func (s *Store) Get(k string) ([]byte, error) {
339342
return nil, fmt.Errorf(failureWhileScanningRow, err)
340343
}
341344

342-
storedValue, err := getValueFromRawDoc(rawDoc, payloadFieldKey)
345+
storedValue, err := getStringValueFromRawDoc(rawDoc, payloadFieldKey)
343346
if err != nil {
344347
return nil, fmt.Errorf("failed to get payload from raw document: %w", err)
345348
}
@@ -377,7 +380,21 @@ func (s *Store) GetTags(k string) ([]newstorage.Tag, error) {
377380
// GetBulk fetches the values associated with the given keys.
378381
// If a key doesn't exist, then a nil []byte is returned for that value. It is not considered an error.
379382
func (s *Store) GetBulk(keys ...string) ([][]byte, error) {
380-
return nil, errors.New("not implemented")
383+
if keys == nil {
384+
return nil, errors.New("keys string slice cannot be nil")
385+
}
386+
387+
rawDocs, err := s.getRawDocs(keys)
388+
if err != nil {
389+
return nil, fmt.Errorf("failure while getting raw CouchDB documents: %w", err)
390+
}
391+
392+
values, err := getPayloadsFromRawDocs(rawDocs)
393+
if err != nil {
394+
return nil, fmt.Errorf("failure while getting stored values from raw docs: %w", err)
395+
}
396+
397+
return values, nil
381398
}
382399

383400
// Query returns all data that satisfies the expression. Expression format: TagName:TagValue.
@@ -535,14 +552,39 @@ func (s *Store) getRevID(k string) (string, error) {
535552
return "", err
536553
}
537554

538-
revID, err := getValueFromRawDoc(rawDoc, revIDFieldKey)
555+
revID, err := getStringValueFromRawDoc(rawDoc, revIDFieldKey)
539556
if err != nil {
540557
return "", fmt.Errorf("failed to get revision ID from the raw document: %w", err)
541558
}
542559

543560
return revID, nil
544561
}
545562

563+
// getRawDocs returns the raw documents from CouchDB using a bulk REST call.
564+
// If a document is not found, then the raw document will be nil. It is not considered an error.
565+
func (s *Store) getRawDocs(keys []string) ([]map[string]interface{}, error) {
566+
bulkGetReferences := make([]kivik.BulkGetReference, len(keys))
567+
for i, key := range keys {
568+
bulkGetReferences[i].ID = key
569+
}
570+
571+
rows, err := s.db.BulkGet(context.Background(), bulkGetReferences)
572+
if err != nil {
573+
return nil, fmt.Errorf("failure while sending request to CouchDB bulk docs endpoint: %w", err)
574+
}
575+
576+
rawDocs, err := getRawDocsFromRows(rows)
577+
if err != nil {
578+
return nil, fmt.Errorf("failed to get raw documents from rows: %w", err)
579+
}
580+
581+
if len(rawDocs) != len(keys) {
582+
return nil, fmt.Errorf("received %d raw documents, but %d were expected", len(rawDocs), len(keys))
583+
}
584+
585+
return rawDocs, nil
586+
}
587+
546588
type couchDBResultsIterator struct {
547589
store *Store
548590
resultRows rows
@@ -733,15 +775,12 @@ func createIndexes(db *kivik.DB, tagNamesNeedIndexCreation []string) error {
733775

734776
func getQueryOptions(options []newstorage.QueryOption) newstorage.QueryOptions {
735777
var queryOptions newstorage.QueryOptions
778+
queryOptions.PageSize = 25
736779

737780
for _, option := range options {
738781
option(&queryOptions)
739782
}
740783

741-
if queryOptions.PageSize == 0 {
742-
queryOptions.PageSize = 25
743-
}
744-
745784
return queryOptions
746785
}
747786

@@ -753,7 +792,7 @@ func getValueFromRows(rows rows, rawDocKey string) (string, error) {
753792
return "", fmt.Errorf(failWhileScanResultRows, err)
754793
}
755794

756-
value, err := getValueFromRawDoc(rawDoc, rawDocKey)
795+
value, err := getStringValueFromRawDoc(rawDoc, rawDocKey)
757796
if err != nil {
758797
return "", fmt.Errorf(`failure while getting the value associated with the "%s" key`+
759798
`from the raw document`, rawDocKey)
@@ -762,7 +801,7 @@ func getValueFromRows(rows rows, rawDocKey string) (string, error) {
762801
return value, nil
763802
}
764803

765-
func getValueFromRawDoc(rawDoc map[string]interface{}, rawDocKey string) (string, error) {
804+
func getStringValueFromRawDoc(rawDoc map[string]interface{}, rawDocKey string) (string, error) {
766805
value, ok := rawDoc[rawDocKey]
767806
if !ok {
768807
return "", fmt.Errorf(`"%s" is missing from the raw document`, rawDocKey)
@@ -778,6 +817,70 @@ func getValueFromRawDoc(rawDoc map[string]interface{}, rawDocKey string) (string
778817
return valueString, nil
779818
}
780819

820+
func getPayloadsFromRawDocs(rawDocs []map[string]interface{}) ([][]byte, error) {
821+
storedValues := make([][]byte, len(rawDocs))
822+
823+
for i, rawDoc := range rawDocs {
824+
// If the rawDoc is nil, this means that the value could not be found.
825+
// It is not considered an error.
826+
if rawDoc == nil {
827+
storedValues[i] = nil
828+
829+
continue
830+
}
831+
832+
// CouchDB still returns a raw document if the key has been deleted, so if this is a "deleted" raw document
833+
// then we need to return nil to indicate that the value could not be found
834+
isDeleted, containsIsDeleted := rawDoc[deletedFieldKey]
835+
if containsIsDeleted {
836+
isDeletedBool, ok := isDeleted.(bool)
837+
if !ok {
838+
return nil, errors.New("failed to assert the retrieved deleted field value as a bool")
839+
}
840+
841+
if isDeletedBool {
842+
storedValues[i] = nil
843+
844+
continue
845+
}
846+
}
847+
848+
storedValue, err := getStringValueFromRawDoc(rawDoc, payloadFieldKey)
849+
if err != nil {
850+
return nil, fmt.Errorf(`failed to get the payload from the raw document: %w`, err)
851+
}
852+
853+
storedValues[i] = []byte(storedValue)
854+
}
855+
856+
return storedValues, nil
857+
}
858+
859+
func getRawDocsFromRows(rows rows) ([]map[string]interface{}, error) {
860+
moreDocumentsToRead := rows.Next()
861+
862+
var rawDocs []map[string]interface{}
863+
864+
for moreDocumentsToRead {
865+
var rawDoc map[string]interface{}
866+
err := rows.ScanDoc(&rawDoc)
867+
// For the regular Get method, Kivik actually returns a different error message if a document was deleted.
868+
// When doing a bulk get, however, Kivik doesn't return an error message, and we have to check the "_deleted"
869+
// field in the raw doc later. This is done in the getPayloadsFromRawDocs method.
870+
// If the document wasn't found, we allow the nil raw doc to be appended since we don't consider it to be
871+
// an error.
872+
if err != nil && !strings.Contains(err.Error(), bulkGetDocNotFoundErrMsgFromKivik) {
873+
return nil, fmt.Errorf(failWhileScanResultRows, err)
874+
}
875+
876+
rawDocs = append(rawDocs, rawDoc)
877+
878+
moreDocumentsToRead = rows.Next()
879+
}
880+
881+
return rawDocs, nil
882+
}
883+
781884
func getTagsFromRawDoc(rawDoc map[string]interface{}) ([]newstorage.Tag, error) {
782885
var tags []newstorage.Tag
783886

component/newstorage/couchdb/store_internal_test.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type mockDB struct {
2121
errPut error
2222
getRowBodyData string
2323
errGetRow error
24+
errBulkGet error
2425
}
2526

2627
func (m *mockDB) Get(context.Context, string, ...kivik.Options) *kivik.Row {
@@ -42,17 +43,22 @@ func (m *mockDB) Delete(context.Context, string, string, ...kivik.Options) (stri
4243
return "", errors.New("mockDB Delete always fails")
4344
}
4445

46+
func (m *mockDB) BulkGet(context.Context, []kivik.BulkGetReference, ...kivik.Options) (*kivik.Rows, error) {
47+
return &kivik.Rows{}, m.errBulkGet
48+
}
49+
4550
func (m *mockDB) Close(context.Context) error {
4651
return errors.New("mockDB Close always fails")
4752
}
4853

4954
type mockRows struct {
5055
err error
5156
errClose error
57+
next bool
5258
}
5359

5460
func (m *mockRows) Next() bool {
55-
return false
61+
return m.next
5662
}
5763

5864
func (m *mockRows) Err() error {
@@ -174,6 +180,17 @@ func TestStore_Get_Internal(t *testing.T) {
174180
})
175181
}
176182

183+
func TestStore_GetBulk_Internal(t *testing.T) {
184+
t.Run("Failure while getting raw CouchDB documents", func(t *testing.T) {
185+
store := &Store{db: &mockDB{errBulkGet: errors.New("mockDB BulkGet always fails")}}
186+
187+
values, err := store.GetBulk("key")
188+
require.EqualError(t, err, "failure while getting raw CouchDB documents: "+
189+
"failure while sending request to CouchDB bulk docs endpoint: mockDB BulkGet always fails")
190+
require.Nil(t, values)
191+
})
192+
}
193+
177194
func TestStore_Query_Internal(t *testing.T) {
178195
t.Run("Failure sending tag name only query to find endpoint", func(t *testing.T) {
179196
store := &Store{db: &mockDB{}}
@@ -217,6 +234,35 @@ func TestStore_Delete_Internal(t *testing.T) {
217234
})
218235
}
219236

237+
func TestGetRawDocsFromRows(t *testing.T) {
238+
t.Run("Failure while scanning result rows", func(t *testing.T) {
239+
rawDocs, err := getRawDocsFromRows(&mockRows{next: true})
240+
require.EqualError(t, err, "failure while scanning result rows: mockRows ScanDoc always fails")
241+
require.Nil(t, rawDocs)
242+
})
243+
}
244+
245+
func TestPayloadsFromRawDocs(t *testing.T) {
246+
t.Run("Failed to assert deleted field value as a bool", func(t *testing.T) {
247+
rawDocs := make([]map[string]interface{}, 1)
248+
rawDocs[0] = make(map[string]interface{})
249+
rawDocs[0][deletedFieldKey] = "Not a bool"
250+
251+
payloads, err := getPayloadsFromRawDocs(rawDocs)
252+
require.EqualError(t, err, "failed to assert the retrieved deleted field value as a bool")
253+
require.Nil(t, payloads)
254+
})
255+
t.Run("Failed to get the payload from the raw document", func(t *testing.T) {
256+
rawDocs := make([]map[string]interface{}, 1)
257+
rawDocs[0] = make(map[string]interface{})
258+
259+
payloads, err := getPayloadsFromRawDocs(rawDocs)
260+
require.EqualError(t, err,
261+
`failed to get the payload from the raw document: "payload" is missing from the raw document`)
262+
require.Nil(t, payloads)
263+
})
264+
}
265+
220266
func TestCouchDBResultsIterator_Next_Internal(t *testing.T) {
221267
t.Run("Error returned from result rows", func(t *testing.T) {
222268
iterator := &couchDBResultsIterator{

component/newstorage/couchdb/store_test.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -375,10 +375,18 @@ func TestStore_GetTags(t *testing.T) {
375375
}
376376

377377
func TestStore_GetBulk(t *testing.T) {
378-
t.Run("Failure - not implemented", func(t *testing.T) {
379-
store := &Store{}
380-
_, err := store.GetBulk()
381-
require.EqualError(t, err, "not implemented")
378+
t.Run("Failure: keys string slice cannot be nil", func(t *testing.T) {
379+
provider, err := NewProvider(couchDBURL, WithDBPrefix("prefix"))
380+
require.NoError(t, err)
381+
require.NotNil(t, provider)
382+
383+
store, err := provider.OpenStore(randomStoreName())
384+
require.NoError(t, err)
385+
require.NotNil(t, store)
386+
387+
values, err := store.GetBulk(nil...)
388+
require.EqualError(t, err, "keys string slice cannot be nil")
389+
require.Nil(t, values)
382390
})
383391
}
384392

0 commit comments

Comments
 (0)