diff --git a/driver/driver.go b/driver/driver.go index 2522dcf3a..3ee13ce70 100644 --- a/driver/driver.go +++ b/driver/driver.go @@ -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 diff --git a/find.go b/find.go index d418bdb62..ccbfdfa7f 100644 --- a/find.go +++ b/find.go @@ -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 diff --git a/find_test.go b/find_test.go index 431097a56..1ebfcee00 100644 --- a/find_test.go +++ b/find_test.go @@ -14,6 +14,7 @@ package kivik import ( "context" + "encoding/json" "errors" "fmt" "net/http" @@ -31,6 +32,7 @@ func TestFind(t *testing.T) { name string db *DB query interface{} + options []Option expected *ResultSet status int err string @@ -63,7 +65,7 @@ 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) } @@ -71,7 +73,11 @@ func TestFind(t *testing.T) { }, }, }, - 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{ @@ -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) diff --git a/mockdb/db_test.go b/mockdb/db_test.go index 2388bab9f..805682dee 100644 --- a/mockdb/db_test.go +++ b/mockdb/db_test.go @@ -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) } @@ -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) } @@ -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) } @@ -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) }