From 746fb4de5489d1b9516da87134349ee2cdb2876c Mon Sep 17 00:00:00 2001 From: Jonathan Hall Date: Mon, 9 Oct 2017 18:10:02 +0200 Subject: [PATCH 1/4] Add support for query plan explanations. --- driver/driver.go | 21 ++++++++++++++++++++- find.go | 27 ++++++++++++++++++++++++++ find_test.go | 49 ++++++++++++++++++++++++++++++++++++++++++++++++ mock_test.go | 25 ++++++++++++++++++++++++ 4 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 find_test.go create mode 100644 mock_test.go diff --git a/driver/driver.go b/driver/driver.go index 69c1799fb..d8efd549d 100644 --- a/driver/driver.go +++ b/driver/driver.go @@ -168,7 +168,7 @@ type BulkDocer interface { BulkDocs(ctx context.Context, docs []interface{}) (BulkResults, error) } -// Finder is an optional interface which may be implemented by a database. The +// The Finder is an optional interface which may be implemented by a database. The // Finder interface provides access to the new (in CouchDB 2.0) MongoDB-style // query interface. type Finder interface { @@ -188,6 +188,25 @@ type Finder interface { DeleteIndex(ctx context.Context, ddoc, name string) error } +// QueryPlan is the response of an Explain query. +type QueryPlan struct { + DBName string `json:"dbname"` + Index map[string]interface{} `json:"index"` + Selector map[string]interface{} `json:"selector"` + Options map[string]interface{} `json:"opts"` + Limit int64 `json:"limit"` + Skip int64 `json:"skip"` + Fields []interface{} `json:"fields"` + Range map[string]interface{} `json:"range"` +} + +// The Explainer is an optional interface which provides access to the query +// explanation API supported by CouchDB 2.0 and newer, and PouchDB 6.3.4 and +// newer. +type Explainer interface { + Explain(ctx context.Context, query interface{}) (*QueryPlan, error) +} + // Index is a MonboDB-style index definition. type Index struct { DesignDoc string `json:"ddoc,omitempty"` diff --git a/find.go b/find.go index cbb3475f3..8f7b7c0af 100644 --- a/find.go +++ b/find.go @@ -62,3 +62,30 @@ func (db *DB) GetIndexes(ctx context.Context) ([]Index, error) { } return nil, findNotImplemented } + +// QueryPlan is the query execution plan for a query, as returned by the Explain +// function. +type QueryPlan struct { + DBName string `json:"dbname"` + Index map[string]interface{} `json:"index"` + Selector map[string]interface{} `json:"selector"` + Options map[string]interface{} `json:"opts"` + Limit int64 `json:"limit"` + Skip int64 `json:"skip"` + Fields []interface{} `json:"fields"` + Range map[string]interface{} `json:"range"` +} + +// Explain returns the query plan for a given query. Explain takes the same +// arguments as Find. +func (db *DB) Explain(ctx context.Context, query interface{}) (*QueryPlan, error) { + if explainer, ok := db.driverDB.(driver.Explainer); ok { + plan, err := explainer.Explain(ctx, query) + if err != nil { + return nil, err + } + qp := QueryPlan(*plan) + return &qp, nil + } + return nil, errors.Status(StatusNotImplemented, "kivik: driver does not support explain") +} diff --git a/find_test.go b/find_test.go new file mode 100644 index 000000000..b4f95a8d3 --- /dev/null +++ b/find_test.go @@ -0,0 +1,49 @@ +package kivik + +import ( + "context" + "errors" + "testing" + + "github.com/flimzy/diff" + "github.com/flimzy/kivik/driver" + "github.com/flimzy/testy" +) + +func TestExplain(t *testing.T) { + tests := []struct { + name string + db driver.DB + expected *QueryPlan + status int + err string + }{ + { + name: "non-finder", + db: &mockDB{}, + status: StatusNotImplemented, + err: "kivik: driver does not support explain", + }, + { + name: "explain error", + db: &mockExplainer{err: errors.New("explain error")}, + status: StatusInternalServerError, + err: "explain error", + }, + { + name: "success", + db: &mockExplainer{plan: &driver.QueryPlan{DBName: "foo"}}, + expected: &QueryPlan{DBName: "foo"}, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + db := &DB{driverDB: test.db} + result, err := db.Explain(context.Background(), nil) + testy.StatusError(t, test.err, test.status, err) + if d := diff.Interface(test.expected, result); d != nil { + t.Error(d) + } + }) + } +} diff --git a/mock_test.go b/mock_test.go new file mode 100644 index 000000000..e7594c672 --- /dev/null +++ b/mock_test.go @@ -0,0 +1,25 @@ +package kivik + +import ( + "context" + + "github.com/flimzy/kivik/driver" +) + +type mockDB struct { + driver.DB +} + +var _ driver.DB = &mockDB{} + +type mockExplainer struct { + driver.DB + plan *driver.QueryPlan + err error +} + +var _ driver.Explainer = &mockExplainer{} + +func (db *mockExplainer) Explain(_ context.Context, query interface{}) (*driver.QueryPlan, error) { + return db.plan, db.err +} From a4a24fab6f83d2ce0fc5a57b7d1424f3d6023ab7 Mon Sep 17 00:00:00 2001 From: Jonathan Hall Date: Mon, 9 Oct 2017 20:21:00 +0200 Subject: [PATCH 2/4] Explain particulars of the Fields field in the query plan --- driver/driver.go | 7 +++++-- find.go | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/driver/driver.go b/driver/driver.go index d8efd549d..b4f6a7d2a 100644 --- a/driver/driver.go +++ b/driver/driver.go @@ -196,8 +196,11 @@ type QueryPlan struct { Options map[string]interface{} `json:"opts"` Limit int64 `json:"limit"` Skip int64 `json:"skip"` - Fields []interface{} `json:"fields"` - Range map[string]interface{} `json:"range"` + + // Fields is the list of fields to be returned in the result set, or + // an empty list if all fields are to be returned. + Fields []interface{} `json:"fields"` + Range map[string]interface{} `json:"range"` } // The Explainer is an optional interface which provides access to the query diff --git a/find.go b/find.go index 8f7b7c0af..a24337d14 100644 --- a/find.go +++ b/find.go @@ -72,8 +72,11 @@ type QueryPlan struct { Options map[string]interface{} `json:"opts"` Limit int64 `json:"limit"` Skip int64 `json:"skip"` - Fields []interface{} `json:"fields"` - Range map[string]interface{} `json:"range"` + + // Fields is the list of fields to be returned in the result set, or + // an empty list if all fields are to be returned. + Fields []interface{} `json:"fields"` + Range map[string]interface{} `json:"range"` } // Explain returns the query plan for a given query. Explain takes the same From 7171ed1314a18aa632ad454a2c47695b3500d557 Mon Sep 17 00:00:00 2001 From: Jonathan Hall Date: Mon, 9 Oct 2017 20:23:25 +0200 Subject: [PATCH 3/4] Add test dep --- glide.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/glide.yaml b/glide.yaml index aa114cc0d..56ec23cca 100644 --- a/glide.yaml +++ b/glide.yaml @@ -27,3 +27,6 @@ import: - package: github.com/go-kivik/kiviktest subpackages: - kt +testimport: +- package: github.com/flimzy/testy + version: ~0.0.1 From 600de2c2ac9de29c1239aeefe2f8c881037d6415 Mon Sep 17 00:00:00 2001 From: Jonathan Hall Date: Mon, 9 Oct 2017 21:02:39 +0200 Subject: [PATCH 4/4] Attempt to work around OSX build issue --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index cd9b1f2e4..1954a96cf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,6 +29,7 @@ env: global: - KIVIK_TEST_DSN_COUCH16="http://admin:abc123@localhost:6000/" - KIVIK_TEST_DSN_COUCH20="http://admin:abc123@localhost:6001/" + - HOMEBREW_NO_AUTO_UPDATE=1 matrix: - MODE=standard