Skip to content

Commit

Permalink
Merge pull request #1016 from go-kivik/driverFinder
Browse files Browse the repository at this point in the history
Merge Find() options into query
  • Loading branch information
flimzy authored Jul 17, 2024
2 parents bb46ed0 + 759f4d0 commit 4fcf41d
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 22 deletions.
9 changes: 6 additions & 3 deletions driver/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,9 +310,12 @@ type BulkDocer interface {
// Finder is an optional interface which may be implemented by a [DB]. It
// provides access to the MongoDB-style query interface added in CouchDB 2.
type Finder interface {
// Find executes a query using the new /_find interface. If query is a
// string, []byte, or [encoding/json.RawMessage], it should be treated as a
// raw JSON payload. Any other type should be marshaled to JSON.
// Find executes a query using the new /_find interface. query is always
// converted to a [encoding/json.RawMessage] value before passing it to the
// driver. The type remains `interface{}` for backward compatibility, but
// will change with Kivik 5.x. See [issue #1015] for details.
//
// [issue #1014]: https://github.com/go-kivik/kivik/issues/1015
Find(ctx context.Context, query interface{}, options Options) (Rows, error)
// CreateIndex creates an index if it doesn't already exist. If the index
// already exists, it should do nothing. ddoc and name may be empty, in
Expand Down
73 changes: 61 additions & 12 deletions find.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,80 @@ package kivik

import (
"context"
"encoding/json"
"net/http"

"github.com/go-kivik/kivik/v4/driver"
"github.com/go-kivik/kivik/v4/int/errors"
)

// Find executes a query using the [_find interface]. The query must be
// JSON-marshalable to a valid query.
// Find executes a query using the [_find interface]. The query must be a
// string, []byte, or [encoding/json.RawMessage] value, or JSON-marshalable to a
// valid valid query. The options are merged with the query, and will overwrite
// any values in the query.
//
// This arguments this method accepts will change in Kivik 5.x, to be more
// consistent with the rest of the Kivik API. See [issue #1014] for details.
//
// [_find interface]: https://docs.couchdb.org/en/stable/api/database/find.html
// [issue #1014]: https://github.com/go-kivik/kivik/issues/1014
func (db *DB) Find(ctx context.Context, query interface{}, options ...Option) *ResultSet {
if db.err != nil {
return &ResultSet{iter: errIterator(db.err)}
}
if finder, ok := db.driverDB.(driver.Finder); ok {
endQuery, err := db.startQuery()
if err != nil {
return &ResultSet{iter: errIterator(err)}
}
rowsi, err := finder.Find(ctx, query, multiOptions(options))
finder, ok := db.driverDB.(driver.Finder)
if !ok {
return &ResultSet{iter: errIterator(errFindNotImplemented)}
}

jsonQuery, err := toQuery(query, options...)
if err != nil {
return &ResultSet{iter: errIterator(err)}
}

endQuery, err := db.startQuery()
if err != nil {
return &ResultSet{iter: errIterator(err)}
}
rowsi, err := finder.Find(ctx, jsonQuery, multiOptions(options))
if err != nil {
endQuery()
return &ResultSet{iter: errIterator(err)}
}
return newResultSet(ctx, endQuery, rowsi)
}

// toQuery combines query and options into a final JSON query to be sent to the
// driver.
func toQuery(query interface{}, options ...Option) (json.RawMessage, error) {
var queryJSON []byte
switch t := query.(type) {
case string:
queryJSON = []byte(t)
case []byte:
queryJSON = t
case json.RawMessage:
queryJSON = t
default:
var err error
queryJSON, err = json.Marshal(query)
if err != nil {
endQuery()
return &ResultSet{iter: errIterator(err)}
return nil, &errors.Error{Status: http.StatusBadRequest, Err: err}
}
return newResultSet(ctx, endQuery, rowsi)
}
return &ResultSet{iter: errIterator(errFindNotImplemented)}
var queryObject map[string]interface{}
if err := json.Unmarshal(queryJSON, &queryObject); err != nil {
return nil, &errors.Error{Status: http.StatusBadRequest, Err: err}
}

opts := map[string]interface{}{}
multiOptions(options).Apply(opts)

for k, v := range opts {
queryObject[k] = v
}

return json.Marshal(queryObject)
}

// CreateIndex creates an index if it doesn't already exist. ddoc and name may
Expand Down
12 changes: 9 additions & 3 deletions find_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ package kivik

import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
Expand All @@ -31,6 +32,7 @@ func TestFind(t *testing.T) {
name string
db *DB
query interface{}
options []Option
expected *ResultSet
status int
err string
Expand Down Expand Up @@ -63,15 +65,19 @@ func TestFind(t *testing.T) {
client: &Client{},
driverDB: &mock.Finder{
FindFunc: func(_ context.Context, query interface{}, _ driver.Options) (driver.Rows, error) {
expectedQuery := int(3)
expectedQuery := json.RawMessage(`{"limit":3,"selector":{"foo":"bar"},"skip":10}`)
if d := testy.DiffInterface(expectedQuery, query); d != nil {
return nil, fmt.Errorf("Unexpected query:\n%s", d)
}
return &mock.Rows{ID: "a"}, nil
},
},
},
query: int(3),
query: map[string]interface{}{"selector": map[string]interface{}{"foo": "bar"}},
options: []Option{
Param("limit", 3),
Param("skip", 10),
},
expected: &ResultSet{
iter: &iter{
feed: &rowsIterator{
Expand Down Expand Up @@ -105,7 +111,7 @@ func TestFind(t *testing.T) {

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
rs := test.db.Find(context.Background(), test.query)
rs := test.db.Find(context.Background(), test.query, test.options...)
err := rs.Err()
if d := internal.StatusErrorDiff(test.err, test.status, err); d != "" {
t.Error(d)
Expand Down
8 changes: 4 additions & 4 deletions mockdb/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,7 @@ func TestFind(t *testing.T) {
},
test: func(t *testing.T, c *kivik.Client) {
db := c.DB("foo")
rows := db.Find(context.TODO(), 321)
rows := db.Find(context.TODO(), map[string]interface{}{"selector": map[string]interface{}{"foo": "123"}})
if err := rows.Err(); !testy.ErrorMatchesRE("has query: 123", err) {
t.Errorf("Unexpected error: %s", err)
}
Expand All @@ -505,7 +505,7 @@ func TestFind(t *testing.T) {
},
test: func(t *testing.T, c *kivik.Client) {
db := c.DB("foo")
rows := db.Find(context.TODO(), 7)
rows := db.Find(context.TODO(), map[string]interface{}{})
if err := rows.Err(); !testy.ErrorMatches("", err) {
t.Errorf("Unexpected error: %s", err)
}
Expand Down Expand Up @@ -543,7 +543,7 @@ func TestFind(t *testing.T) {
},
test: func(t *testing.T, c *kivik.Client) {
db := c.DB("foo")
rows := db.Find(newCanceledContext(), 0)
rows := db.Find(newCanceledContext(), map[string]interface{}{})
if err := rows.Err(); !testy.ErrorMatches("context canceled", err) {
t.Errorf("Unexpected error: %s", err)
}
Expand All @@ -561,7 +561,7 @@ func TestFind(t *testing.T) {
test: func(t *testing.T, c *kivik.Client) {
foo := c.DB("foo")
_ = c.DB("bar")
rows := foo.Find(context.TODO(), 1)
rows := foo.Find(context.TODO(), map[string]interface{}{})
if err := rows.Err(); !testy.ErrorMatchesRE(`Expected: call to DB\(bar`, err) {
t.Errorf("Unexpected error: %s", err)
}
Expand Down

0 comments on commit 4fcf41d

Please sign in to comment.