Skip to content

Commit

Permalink
Merge pull request #935 from go-kivik/revsdiff
Browse files Browse the repository at this point in the history
Add _revs_diff support to sqlite backend
  • Loading branch information
flimzy committed Apr 13, 2024
2 parents 388f4b0 + 2eca752 commit 0e242a5
Show file tree
Hide file tree
Showing 6 changed files with 299 additions and 47 deletions.
4 changes: 1 addition & 3 deletions couchdb/rows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,9 +238,7 @@ func TestRowsIteratorErrors(t *testing.T) {
if d := internal.StatusErrorDiff(test.err, test.status, err); d != "" {
t.Error(d)
}
if err != nil {
return
}
return
}
})
}
Expand Down
1 change: 1 addition & 0 deletions x/sqlite/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
type DB interface {
driver.DB
driver.Purger
driver.RevsDiffer
}

type testDB struct {
Expand Down
4 changes: 0 additions & 4 deletions x/sqlite/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,3 @@ func (db) DesignDocs(context.Context, driver.Options) (driver.Rows, error) {
func (db) LocalDocs(context.Context, driver.Options) (driver.Rows, error) {
return nil, nil
}

func (db) RevsDiff(context.Context, interface{}) (driver.Rows, error) {
return nil, nil
}
168 changes: 168 additions & 0 deletions x/sqlite/revsdiff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// http:https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.

package sqlite

import (
"bytes"
"context"
"database/sql"
"encoding/json"
"fmt"
"io"
"net/http"
"sort"

"github.com/go-kivik/kivik/v4/driver"
"github.com/go-kivik/kivik/v4/internal"
)

func toRevDiffRequest(revMap interface{}) (map[string][]string, error) {
data, err := json.Marshal(revMap)
if err != nil {
return nil, &internal.Error{Message: "invalid body", Status: http.StatusBadRequest}
}
var req map[string][]string
if err := json.Unmarshal(data, &req); err != nil {
return nil, &internal.Error{Message: "invalid body", Status: http.StatusBadRequest}
}
return req, nil
}

func (d *db) RevsDiff(ctx context.Context, revMap interface{}) (driver.Rows, error) {
req, err := toRevDiffRequest(revMap)
if err != nil {
return nil, err
}

ids := make([]interface{}, 0, len(req))
for id := range req {
ids = append(ids, id)
}

query := fmt.Sprintf(d.query(`
SELECT
id,
rev,
rev_count
FROM (
SELECT
id,
rev || '-' || rev_id AS rev,
COUNT(*) OVER (PARTITION BY id) AS rev_count
FROM {{ .Docs }}
WHERE id IN (%s)
ORDER BY id, rev, rev_id
)
`), placeholders(1, len(ids)))

rows, err := d.db.QueryContext(ctx, query, ids...) //nolint:rowserrcheck // Err checked in Next
if err != nil {
return nil, err
}

return &revsDiffResponse{
rows: rows,
req: req,
}, nil
}

type revsDiffResponse struct {
rows *sql.Rows
req map[string][]string
sortedDocIDs []string
}

var _ driver.Rows = (*revsDiffResponse)(nil)

func (r *revsDiffResponse) Next(row *driver.Row) error {
var (
id string
revs = map[string]struct{}{}
revCount int
)

for {
if !r.rows.Next() {
if err := r.rows.Err(); err != nil {
return err
}
if len(r.req) > 0 {
if len(r.sortedDocIDs) == 0 {
// First time, we need to sort the remaining doc IDs.
r.sortedDocIDs = make([]string, 0, len(r.req))
for id := range r.req {
r.sortedDocIDs = append(r.sortedDocIDs, id)
}
sort.Strings(r.sortedDocIDs)
}

row.ID = r.sortedDocIDs[0]
revs := r.req[row.ID]
sort.Strings(revs)
row.Value = jsonToReader(driver.RevDiff{
Missing: revs,
})
delete(r.req, row.ID)
r.sortedDocIDs = r.sortedDocIDs[1:]
return nil
}
return io.EOF
}
var (
rowID *string
rev string
)
if err := r.rows.Scan(&id, &rev, &rowID, &revCount); err != nil {
return err
}
if rowID != nil {
id = *rowID
}
revs[id] = struct{}{}
if len(revs) == revCount {
break
}
}
row.ID = id
missing := make([]string, 0, len(r.req[id]))
for _, rev := range r.req[id] {
if _, ok := revs[rev]; !ok {
missing = append(missing, rev)
}
}
row.Value = jsonToReader(driver.RevDiff{
Missing: missing,
})
return nil
}

type errorReadCloser struct{ error }

func (e errorReadCloser) Read([]byte) (int, error) { return 0, error(e) }
func (e errorReadCloser) Close() error { return error(e) }

func jsonToReader(i interface{}) io.ReadCloser {
value, err := json.Marshal(i)
if err != nil {
return errorReadCloser{err}
}
return io.NopCloser(bytes.NewReader(value))
}

func (r *revsDiffResponse) Close() error {
return r.rows.Close()
}

func (*revsDiffResponse) Offset() int64 { return 0 }
func (*revsDiffResponse) TotalRows() int64 { return 0 }
func (*revsDiffResponse) UpdateSeq() string { return "" }
82 changes: 82 additions & 0 deletions x/sqlite/revsdiff_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// http:https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.

//go:build !js
// +build !js

package sqlite

import (
"context"
"net/http"
"testing"

"gitlab.com/flimzy/testy"

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

func TestDBRevsDiff(t *testing.T) {
t.Parallel()
type test struct {
db *testDB
revMap interface{}
want []rowResult
wantErr string
wantStatus int
}
tests := testy.NewTable()
tests.Add("invalid revMap", test{
revMap: "foo",
wantErr: "invalid body",
wantStatus: http.StatusBadRequest,
})
tests.Add("empty revMap", test{
revMap: map[string][]string{},
want: nil,
})
tests.Add("all missing", test{
revMap: map[string][]string{
"foo": {"1-abc", "2-def"},
"bar": {"3-ghi"},
},
want: []rowResult{
{ID: "bar", Value: `{"missing":["3-ghi"]}`},
{ID: "foo", Value: `{"missing":["1-abc","2-def"]}`},
},
})

/*
TODO:
- populate `possible_ancestors`
*/

tests.Run(t, func(t *testing.T, tt test) {
t.Parallel()
db := tt.db
if db == nil {
db = newDB(t)
}
rows, err := db.RevsDiff(context.Background(), tt.revMap)
if !testy.ErrorMatches(tt.wantErr, err) {
t.Errorf("Unexpected error: %s", err)
}
if status := kivik.HTTPStatus(err); status != tt.wantStatus {
t.Errorf("Unexpected status: %d", status)
}
if err != nil {
return
}

checkRows(t, rows, tt.want)
})
}
87 changes: 47 additions & 40 deletions x/sqlite/views_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -584,48 +584,55 @@ func TestDBAllDocs(t *testing.T) {
if err != nil {
return
}
// iterate over rows
var got []rowResult

loop:
for {
row := driver.Row{}
err := rows.Next(&row)
switch err {
case io.EOF:
break loop
case driver.EOQ:
continue
case nil:
// continue
default:
t.Fatalf("Next() returned error: %s", err)
}
var errMsg string
if row.Error != nil {
errMsg = row.Error.Error()
}
value, err := io.ReadAll(row.Value)

checkRows(t, rows, tt.want)
})
}

func checkRows(t *testing.T, rows driver.Rows, want []rowResult) {
t.Helper()

// iterate over rows
var got []rowResult

loop:
for {
row := driver.Row{}
err := rows.Next(&row)
switch err {
case io.EOF:
break loop
case driver.EOQ:
continue
case nil:
// continue
default:
t.Fatalf("Next() returned error: %s", err)
}
var errMsg string
if row.Error != nil {
errMsg = row.Error.Error()
}
value, err := io.ReadAll(row.Value)
if err != nil {
t.Fatal(err)
}
var doc []byte
if row.Doc != nil {
doc, err = io.ReadAll(row.Doc)
if err != nil {
t.Fatal(err)
}
var doc []byte
if row.Doc != nil {
doc, err = io.ReadAll(row.Doc)
if err != nil {
t.Fatal(err)
}
}
got = append(got, rowResult{
ID: row.ID,
Rev: row.Rev,
Value: string(value),
Doc: string(doc),
Error: errMsg,
})
}
if d := cmp.Diff(tt.want, got); d != "" {
t.Errorf("Unexpected rows:\n%s", d)
}
})
got = append(got, rowResult{
ID: row.ID,
Rev: row.Rev,
Value: string(value),
Doc: string(doc),
Error: errMsg,
})
}
if d := cmp.Diff(want, got); d != "" {
t.Errorf("Unexpected rows:\n%s", d)
}
}

0 comments on commit 0e242a5

Please sign in to comment.