Skip to content

Commit

Permalink
Merge pull request #239 from go-kivik/release3.1
Browse files Browse the repository at this point in the history
Prepare for release of 3.1
  • Loading branch information
flimzy authored May 2, 2020
2 parents 4de1563 + ba0d89f commit 2e585b6
Show file tree
Hide file tree
Showing 22 changed files with 849 additions and 65 deletions.
8 changes: 2 additions & 6 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,11 @@ variables:
- ./script/test_version.sh
- go test -race -tags=livetest ./...

lint:
linter:
stage: test
image: golang:1.14
services: []
before_script:
- ''
image: golangci/golangci-lint:v1.26
script:
- go mod download
- curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.23.7
- golangci-lint run ./...

coverage:
Expand Down
2 changes: 1 addition & 1 deletion chttp/chttp.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const (
// The default UserAgent values
const (
UserAgent = "Kivik chttp"
Version = "3.0.4"
Version = "3.1.0"
)

// Client represents a client connection. It embeds an *http.Client
Expand Down
9 changes: 8 additions & 1 deletion constants.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package couchdb

// Version is the current version of this package.
const Version = "3.0.4"
const Version = "3.1.0"

const (
// OptionFullCommit is the option key used to set the `X-Couch-Full-Commit`
Expand All @@ -20,6 +20,13 @@ const (
// row, err := db.Get(ctx, "doc_id", kivik.Options{couchdb.OptionIfNoneMatch: "1-xxx"})
OptionIfNoneMatch = "If-None-Match"

// OptionPartition instructs supporting methods to limit the query to the
// specified partition. Supported methods are: Query, AllDocs, Find, and
// Explain. Only supported by CouchDB 3.0.0 and newer.
//
// See https://docs.couchdb.org/en/stable/api/partitioned-dbs.html
OptionPartition = "kivik:partition"

// NoMultipartPut instructs the Put() method not to use CouchDB's
// multipart/related upload capabilities. This only affects PUT requests that
// also include attachments.
Expand Down
45 changes: 36 additions & 9 deletions db.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
"net/textproto"
"net/url"
"os"
"path"
"path/filepath"
"reflect"
"sort"
"strconv"
Expand All @@ -31,8 +33,10 @@ type db struct {
}

var _ driver.DB = &db{}
var _ driver.OptsFinder = &db{}
var _ driver.MetaGetter = &db{}
var _ driver.AttachmentMetaGetter = &db{}
var _ driver.PartitionedDB = &db{}

func (d *db) path(path string) string {
url, err := url.Parse(d.dbName + "/" + strings.TrimPrefix(path, "/"))
Expand Down Expand Up @@ -72,19 +76,32 @@ func optionsToParams(opts ...map[string]interface{}) (url.Values, error) {

// rowsQuery performs a query that returns a rows iterator.
func (d *db) rowsQuery(ctx context.Context, path string, opts map[string]interface{}) (driver.Rows, error) {
keys := opts["keys"]
delete(opts, "keys")
payload := make(map[string]interface{})
if keys := opts["keys"]; keys != nil {
delete(opts, "keys")
payload["keys"] = keys
}
rowsInit := newRows
if queries := opts["queries"]; queries != nil {
rowsInit = func(ctx context.Context, r io.ReadCloser) driver.Rows {
return newMultiQueriesRows(ctx, r)
}
delete(opts, "queries")
payload["queries"] = queries
// Funny that this works even in CouchDB 1.x. It seems 1.x just ignores
// extra path elements beyond the view name. So yay for accidental
// backward compatibility!
path = filepath.Join(path, "queries")
}
query, err := optionsToParams(opts)
if err != nil {
return nil, err
}
options := &chttp.Options{Query: query}
method := http.MethodGet
if keys != nil {
if len(payload) > 0 {
method = http.MethodPost
options.GetBody = chttp.BodyEncoder(map[string]interface{}{
"keys": keys,
})
options.GetBody = chttp.BodyEncoder(payload)
options.Header = http.Header{
chttp.HeaderIdempotencyKey: []string{},
}
Expand All @@ -96,12 +113,17 @@ func (d *db) rowsQuery(ctx context.Context, path string, opts map[string]interfa
if err = chttp.ResponseError(resp); err != nil {
return nil, err
}
return newRows(ctx, resp.Body), nil
return rowsInit(ctx, resp.Body), nil
}

// AllDocs returns all of the documents in the database.
func (d *db) AllDocs(ctx context.Context, opts map[string]interface{}) (driver.Rows, error) {
return d.rowsQuery(ctx, "_all_docs", opts)
reqPath := "_all_docs"
if part, ok := opts[OptionPartition].(string); ok {
delete(opts, OptionPartition)
reqPath = path.Join("_partition", part, reqPath)
}
return d.rowsQuery(ctx, reqPath, opts)
}

// DesignDocs returns all of the documents in the database.
Expand All @@ -116,7 +138,12 @@ func (d *db) LocalDocs(ctx context.Context, opts map[string]interface{}) (driver

// Query queries a view.
func (d *db) Query(ctx context.Context, ddoc, view string, opts map[string]interface{}) (driver.Rows, error) {
return d.rowsQuery(ctx, fmt.Sprintf("_design/%s/_view/%s", chttp.EncodeDocID(ddoc), chttp.EncodeDocID(view)), opts)
reqPath := fmt.Sprintf("_design/%s/_view/%s", chttp.EncodeDocID(ddoc), chttp.EncodeDocID(view))
if part, ok := opts[OptionPartition].(string); ok {
delete(opts, OptionPartition)
reqPath = path.Join("_partition", part, reqPath)
}
return d.rowsQuery(ctx, reqPath, opts)
}

// Get fetches the requested document.
Expand Down
143 changes: 143 additions & 0 deletions db_live_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package couchdb_test

import (
"context"
"os"
"testing"

"gitlab.com/flimzy/testy"

kivik "github.com/go-kivik/kivik/v3"
)

func TestQueries_1_x(t *testing.T) {
dsn := os.Getenv("KIVIK_TEST_DSN_COUCH17")
if dsn == "" {
t.Skip("KIVIK_TEST_DSN_COUCH17 not configured")
}

client, err := kivik.New("couch", dsn)
if err != nil {
t.Fatal(err)
}

db := client.DB(context.Background(), "_users")
rows, err := db.AllDocs(context.Background(), map[string]interface{}{
"queries": []map[string]interface{}{
{},
{},
},
})
if err != nil {
t.Fatal(err)
}
defer rows.Close() // nolint:errcheck
result := make([]interface{}, 0)
for rows.Next() {
if rows.EOQ() {
result = append(result, map[string]interface{}{
"EOQ": true,
"total_rows": rows.TotalRows(),
})
continue
}
result = append(result, map[string]interface{}{
"_id": rows.ID(),
})
}
if err := rows.Err(); err != nil {
t.Fatal(err)
}
if d := testy.DiffInterface(testy.Snapshot(t), result); d != nil {
t.Error(d)
}
}

func TestQueries_2_x(t *testing.T) {
dsn := os.Getenv("KIVIK_TEST_DSN_COUCH23")
if dsn == "" {
dsn = os.Getenv("KIVIK_TEST_DSN_COUCH22")
}
if dsn == "" {
t.Skip("Neither KIVIK_TEST_DSN_COUCH22 nor KIVIK_TEST_DSN_COUCH23 configured")
}

client, err := kivik.New("couch", dsn)
if err != nil {
t.Fatal(err)
}

db := client.DB(context.Background(), "_users")
rows, err := db.AllDocs(context.Background(), map[string]interface{}{
"queries": []map[string]interface{}{
{},
{},
},
})
if err != nil {
t.Fatal(err)
}
defer rows.Close() // nolint:errcheck
result := make([]interface{}, 0)
for rows.Next() {
if rows.EOQ() {
result = append(result, map[string]interface{}{
"EOQ": true,
"total_rows": rows.TotalRows(),
})
continue
}
result = append(result, map[string]interface{}{
"_id": rows.ID(),
})
}
if err := rows.Err(); err != nil {
t.Fatal(err)
}
if d := testy.DiffInterface(testy.Snapshot(t), result); d != nil {
t.Error(d)
}
}

func TestQueries_3_x(t *testing.T) {
dsn := os.Getenv("KIVIK_TEST_DSN_COUCH30")
if dsn == "" {
t.Skip("KIVIK_TEST_DSN_COUCH30 not configured")
}

client, err := kivik.New("couch", dsn)
if err != nil {
t.Fatal(err)
}

db := client.DB(context.Background(), "_users")
rows, err := db.AllDocs(context.Background(), map[string]interface{}{
"queries": []map[string]interface{}{
{},
{},
},
})
if err != nil {
t.Fatal(err)
}
defer rows.Close() // nolint:errcheck
result := make([]interface{}, 0)
for rows.Next() {
if rows.EOQ() {
result = append(result, map[string]interface{}{
"EOQ": true,
"total_rows": rows.TotalRows(),
})
continue
}
result = append(result, map[string]interface{}{
"_id": rows.ID(),
})
}
if err := rows.Err(); err != nil {
t.Fatal(err)
}
if d := testy.DiffInterface(testy.Snapshot(t), result); d != nil {
t.Error(d)
}
}
31 changes: 25 additions & 6 deletions db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,19 @@ import (
)

func TestAllDocs(t *testing.T) {
db := newTestDB(nil, errors.New("test error"))
_, err := db.AllDocs(context.Background(), nil)
testy.ErrorRE(t, `Get "?http://example.com/testdb/_all_docs"?: test error`, err)
t.Run("standard", func(t *testing.T) {
db := newTestDB(nil, errors.New("test error"))
_, err := db.AllDocs(context.Background(), nil)
testy.ErrorRE(t, `Get "?http://example.com/testdb/_all_docs"?: test error`, err)
})

t.Run("partitioned", func(t *testing.T) {
db := newTestDB(nil, errors.New("test error"))
_, err := db.AllDocs(context.Background(), map[string]interface{}{
OptionPartition: "a1",
})
testy.ErrorRE(t, `Get "?http://example.com/testdb/_partition/a1/_all_docs"?: test error`, err)
})
}

func TestDesignDocs(t *testing.T) {
Expand All @@ -43,9 +53,18 @@ func TestLocalDocs(t *testing.T) {
}

func TestQuery(t *testing.T) {
db := newTestDB(nil, errors.New("test error"))
_, err := db.Query(context.Background(), "ddoc", "view", nil)
testy.ErrorRE(t, `Get "?http://example.com/testdb/_design/ddoc/_view/view"?: test error`, err)
t.Run("standard", func(t *testing.T) {
db := newTestDB(nil, errors.New("test error"))
_, err := db.Query(context.Background(), "ddoc", "view", nil)
testy.ErrorRE(t, `Get "?http://example.com/testdb/_design/ddoc/_view/view"?: test error`, err)
})
t.Run("partitioned", func(t *testing.T) {
db := newTestDB(nil, errors.New("test error"))
_, err := db.Query(context.Background(), "ddoc", "view", map[string]interface{}{
OptionPartition: "a2",
})
testy.ErrorRE(t, `Get "?http://example.com/testdb/_partition/a2/_design/ddoc/_view/view"?: test error`, err)
})
}

type Attachment struct {
Expand Down
41 changes: 41 additions & 0 deletions dbstats.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,44 @@ func (c *client) DBsStats(ctx context.Context, dbnames []string) ([]*driver.DBSt
}
return stats, nil
}

type partitionStats struct {
DBName string `json:"db_name"`
DocCount int64 `json:"doc_count"`
DocDelCount int64 `json:"doc_del_count"`
Partition string `json:"partition"`
Sizes struct {
Active int64 `json:"active"`
External int64 `json:"external"`
}
rawBody json.RawMessage
}

func (s *partitionStats) UnmarshalJSON(p []byte) error {
c := struct {
partitionStats
UnmarshalJSON struct{}
}{}
if err := json.Unmarshal(p, &c); err != nil {
return err
}
*s = c.partitionStats
s.rawBody = p
return nil
}

func (d *db) PartitionStats(ctx context.Context, name string) (*driver.PartitionStats, error) {
result := partitionStats{}
if _, err := d.Client.DoJSON(ctx, http.MethodGet, d.path("_partition/"+name), nil, &result); err != nil {
return nil, err
}
return &driver.PartitionStats{
DBName: result.DBName,
DocCount: result.DocCount,
DeletedDocCount: result.DocDelCount,
Partition: result.Partition,
ActiveSize: result.Sizes.Active,
ExternalSize: result.Sizes.External,
RawResponse: result.rawBody,
}, nil
}
Loading

0 comments on commit 2e585b6

Please sign in to comment.