Skip to content

Commit

Permalink
Add mango index & queries to couchdb package
Browse files Browse the repository at this point in the history
  • Loading branch information
Romain committed Oct 17, 2016
1 parent 5e3180b commit 39152df
Show file tree
Hide file tree
Showing 6 changed files with 399 additions and 15 deletions.
57 changes: 50 additions & 7 deletions couchdb/couchdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,9 @@ import (
"net/http"
"net/url"
"strings"
)

type updateResponse struct {
ID string `json:"id"`
Rev string `json:"rev"`
Ok bool `json:"ok"`
}
"github.com/cozy/cozy-stack/couchdb/mango"
)

// Doc is the interface that encapsulate a couchdb document, of any
// serializable type. This interface defines method to set and get the
Expand Down Expand Up @@ -197,7 +193,7 @@ func DeleteDB(dbprefix, doctype string) error {
// ResetDB destroy and recreate the database for a doctype
func ResetDB(dbprefix, doctype string) (err error) {
err = DeleteDB(dbprefix, doctype)
if err != nil {
if err != nil && !IsNoDatabaseError(err) {
return err
}
return CreateDB(dbprefix, doctype)
Expand Down Expand Up @@ -312,3 +308,50 @@ func CreateDoc(dbprefix string, doc Doc) (err error) {
doc.SetRev(res.Rev)
return nil
}

// DefineIndex define the index on the doctype database
// see query package on how to define an index
func DefineIndex(dbprefix, doctype string, index mango.IndexDefinitionRequest) error {
url := makeDBName(dbprefix, doctype) + "/_index"
var response indexCreationResponse
return makeRequest("POST", url, &index, &response)
}

// FindDocs returns all documents matching the passed FindRequest
// documents will be unmarshalled in the provided results slice.
func FindDocs(dbprefix, doctype string, req *FindRequest, results interface{}) error {
url := makeDBName(dbprefix, doctype) + "/_find"
// prepare a structure to receive the results
var response = &findResponse{}
err := makeRequest("POST", url, &req, &response)
if err != nil {
return err
}
return json.Unmarshal(response.Docs, results)
}

type indexCreationResponse struct {
Result string `json:"result"`
Error string `json:"error"`
ID string `json:"id"`
Name string `json:"name"`
}

type updateResponse struct {
ID string `json:"id"`
Rev string `json:"rev"`
Ok bool `json:"ok"`
}

type findResponse struct {
Docs json.RawMessage `json:"docs"`
}

// A FindRequest is a structure containin
type FindRequest struct {
Selector mango.Filter `json:"selector"`
Limit int `json:"limit,omitempty"`
Skip int `json:"skip,omitempty"`
Sort *mango.SortBy `json:"sort,omitempty"`
Fields []string `json:"fields,omitempty"`
}
81 changes: 73 additions & 8 deletions couchdb/couchdb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"os"
"testing"

"github.com/cozy/cozy-stack/couchdb/mango"
"github.com/sourcegraph/checkup"
"github.com/stretchr/testify/assert"
)
Expand All @@ -17,10 +18,15 @@ func TestErrors(t *testing.T) {
assert.Contains(t, err.Error(), "missing")
}

const TestDoctype = "io.cozy.testobject"
const TestPrefix = "dev/"

type testDoc struct {
TestID string `json:"_id,omitempty"`
TestRev string `json:"_rev,omitempty"`
Test string `json:"test"`
FieldA string `json:"fieldA,omitempty"`
FieldB int `json:"fieldB,omitempty"`
}

func (t *testDoc) ID() string {
Expand All @@ -32,7 +38,7 @@ func (t *testDoc) Rev() string {
}

func (t *testDoc) DocType() string {
return "io.cozy.testobject"
return TestDoctype
}

func (t *testDoc) SetID(id string) {
Expand All @@ -52,20 +58,19 @@ func makeTestDoc() Doc {
func TestCreateDoc(t *testing.T) {
var err error

var TESTPREFIX = "dev/"
var doc = makeTestDoc()
assert.Empty(t, doc.Rev(), doc.ID())

// Create the document
err = CreateDoc(TESTPREFIX, doc)
err = CreateDoc(TestPrefix, doc)
assert.NoError(t, err)
assert.NotEmpty(t, doc.Rev(), doc.ID())

docType, id := doc.DocType(), doc.ID()

// Fetch it and see if its match
fetched := &testDoc{}
err = GetDoc(TESTPREFIX, docType, id, fetched)
err = GetDoc(TestPrefix, docType, id, fetched)
assert.NoError(t, err)
assert.Equal(t, doc.ID(), fetched.ID())
assert.Equal(t, doc.Rev(), fetched.Rev())
Expand All @@ -76,25 +81,25 @@ func TestCreateDoc(t *testing.T) {
// Update it
updated := fetched
updated.Test = "changedvalue"
err = UpdateDoc(TESTPREFIX, updated)
err = UpdateDoc(TestPrefix, updated)
assert.NoError(t, err)
assert.NotEqual(t, revBackup, updated.Rev())
assert.Equal(t, "changedvalue", updated.Test)

// Refetch it and see if its match
fetched2 := &testDoc{}
err = GetDoc(TESTPREFIX, docType, id, fetched2)
err = GetDoc(TestPrefix, docType, id, fetched2)
assert.NoError(t, err)
assert.Equal(t, doc.ID(), fetched2.ID())
assert.Equal(t, updated.Rev(), fetched2.Rev())
assert.Equal(t, "changedvalue", fetched2.Test)

// Delete it
err = DeleteDoc(TESTPREFIX, updated)
err = DeleteDoc(TestPrefix, updated)
assert.NoError(t, err)

fetched3 := &testDoc{}
err = GetDoc(TESTPREFIX, docType, id, fetched3)
err = GetDoc(TestPrefix, docType, id, fetched3)
assert.Error(t, err)
coucherr, iscoucherr := err.(*Error)
if assert.True(t, iscoucherr) {
Expand All @@ -103,13 +108,73 @@ func TestCreateDoc(t *testing.T) {

}

func TestDefineIndex(t *testing.T) {
err := DefineIndex(TestPrefix, TestDoctype, mango.IndexOnFields("fieldA", "fieldB"))
assert.NoError(t, err)

// if I try to define the same index several time
err2 := DefineIndex(TestPrefix, TestDoctype, mango.IndexOnFields("fieldA", "fieldB"))
assert.NoError(t, err2)
}

func TestQuery(t *testing.T) {

// create a few docs for testing
doc1 := testDoc{FieldA: "value1", FieldB: 100}
doc2 := testDoc{FieldA: "value2", FieldB: 1000}
doc3 := testDoc{FieldA: "value2", FieldB: 300}
doc4 := testDoc{FieldA: "value13", FieldB: 1500}
docs := []*testDoc{&doc1, &doc2, &doc3, &doc4}
for _, doc := range docs {
err := CreateDoc(TestPrefix, doc)
if !assert.NoError(t, err) || doc.ID() == "" {
t.FailNow()
return
}
}

err := DefineIndex(TestPrefix, TestDoctype, mango.IndexOnFields("fieldA", "fieldB"))
if !assert.NoError(t, err) {
t.FailNow()
return
}
var out []testDoc
req := &FindRequest{Selector: mango.Equal("fieldA", "value2")}
err = FindDocs(TestPrefix, TestDoctype, req, &out)
if assert.NoError(t, err) {
assert.Len(t, out, 2, "should get 2 results")
// if fieldA are equaly, docs will be ordered by fieldB
assert.Equal(t, doc3.ID(), out[0].ID())
assert.Equal(t, "value2", out[0].FieldA)
assert.Equal(t, doc2.ID(), out[1].ID())
assert.Equal(t, "value2", out[1].FieldA)
}

var out2 []testDoc
req2 := &FindRequest{Selector: mango.StartWith("fieldA", "value1")}
err = FindDocs(TestPrefix, TestDoctype, req2, &out2)
if assert.NoError(t, err) {
assert.Len(t, out, 2, "should get 2 results")
// if we do as startWith, docs will be ordered by the rest of fieldA
assert.Equal(t, doc1.ID(), out2[0].ID())
assert.Equal(t, doc4.ID(), out2[1].ID())
}

fmt.Println("results", out)
}

func TestMain(m *testing.M) {
// First we make sure couchdb is started
couchdb, err := checkup.HTTPChecker{URL: CouchDBURL}.Check()
if err != nil || couchdb.Status() != checkup.Healthy {
fmt.Println("This test need couchdb to run.")
os.Exit(1)
}
err = ResetDB(TestPrefix, TestDoctype)
if err != nil {
fmt.Printf("Cant reset db (%s, %s) %s\n", TestPrefix, TestDoctype, err.Error())
os.Exit(1)
}

os.Exit(m.Run())
}
27 changes: 27 additions & 0 deletions couchdb/mango/index.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package mango

import "encoding/json"

// An IndexDefinition is just a list of fields to be indexed.
type IndexDefinition []string

// MarshalJSON implements the json.Marshaller interface on IndexDefinition
// by wrapping it in a {"fields": ...}
func (def IndexDefinition) MarshalJSON() ([]byte, error) {
return json.Marshal(makeMap("fields", []string(def)))
}

// An IndexDefinitionRequest is a request to be POSTED to create the index
type IndexDefinitionRequest struct {
Name string `json:"name,omitempty"`
DDoc string `json:"ddoc,omitempty"`
Index IndexDefinition `json:"index"`
}

// IndexOnFields constructs a new IndexDefinitionRequest
// it lets couchdb defaults for index & designdoc names.
func IndexOnFields(fields ...string) IndexDefinitionRequest {
return IndexDefinitionRequest{
Index: IndexDefinition(fields),
}
}
15 changes: 15 additions & 0 deletions couchdb/mango/index_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package mango

import (
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"
)

func TestIndexMarshaling(t *testing.T) {
def := IndexOnFields("folderID", "name")
jsonbytes, _ := json.Marshal(def)
expected := `{"index":{"fields":["folderID","name"]}}`
assert.Equal(t, expected, string(jsonbytes), "index should MarshalJSON properly")
}
Loading

0 comments on commit 39152df

Please sign in to comment.