Skip to content

Commit

Permalink
Merge pull request #932 from go-kivik/purge
Browse files Browse the repository at this point in the history
Basic purge support & more stub methods
  • Loading branch information
flimzy committed Apr 11, 2024
2 parents fb859da + b15d8d1 commit 388f4b0
Show file tree
Hide file tree
Showing 12 changed files with 292 additions and 16 deletions.
9 changes: 7 additions & 2 deletions x/sqlite/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,14 @@ import (
"github.com/go-kivik/kivik/v4/internal/mock"
)

type DB interface {
driver.DB
driver.Purger
}

type testDB struct {
t *testing.T
driver.DB
DB
}

func (tdb *testDB) underlying() *sql.DB {
Expand Down Expand Up @@ -91,7 +96,7 @@ func newDB(t *testing.T) *testDB {
_ = db.Close()
})
return &testDB{
DB: db,
DB: db.(DB),
t: t,
}
}
Expand Down
42 changes: 40 additions & 2 deletions x/sqlite/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ type db struct {

var _ driver.DB = (*db)(nil)

func (d *db) Close() error {
return d.db.Close()
}

func (d *db) Ping(ctx context.Context) error {
return d.db.PingContext(ctx)
}

/* -- stub methods -- */

func (db) Stats(context.Context) (*driver.DBStats, error) {
return nil, nil
}
Expand All @@ -46,6 +56,34 @@ func (db) Query(context.Context, string, string, driver.Options) (driver.Rows, e
return nil, nil
}

func (d *db) Close() error {
return d.db.Close()
func (db) OpenRevs(context.Context, string, []string, driver.Options) (driver.Rows, error) {
return nil, nil
}

func (db) BulkDocs(context.Context, []interface{}, driver.Options) ([]driver.BulkResult, error) {
return nil, nil
}

func (db) GetAttachmentMeta(context.Context, string, string, driver.Options) (*driver.Attachment, error) {
return nil, nil
}

func (db) GetRev(context.Context, string, driver.Options) (string, error) {
return "", nil
}

func (db) Copy(context.Context, string, string, driver.Options) (string, error) {
return "", nil
}

func (db) DesignDocs(context.Context, driver.Options) (driver.Rows, error) {
return nil, nil
}

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
}
7 changes: 3 additions & 4 deletions x/sqlite/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,13 @@ type leaf struct {
ParentRevID *string
}

func readRevisions(t *testing.T, db *sql.DB, id string) []leaf {
func readRevisions(t *testing.T, db *sql.DB) []leaf {
t.Helper()
rows, err := db.Query(`
SELECT id, rev, rev_id, parent_rev, parent_rev_id
FROM "test_revs"
WHERE id=$1
ORDER BY rev, rev_id
`, id)
ORDER BY id, rev, rev_id
`)
if err != nil {
t.Fatal(err)
}
Expand Down
2 changes: 1 addition & 1 deletion x/sqlite/delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ func TestDBDelete(t *testing.T) {
id: "foo",
options: kivik.Rev("not a rev"),
wantStatus: http.StatusBadRequest,
wantErr: `strconv.ParseInt: parsing "not a rev": invalid syntax`,
wantErr: `invalid rev format`,
})

/*
Expand Down
2 changes: 1 addition & 1 deletion x/sqlite/deleteattachment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ func TestDBDeleteAttachment(t *testing.T) {
case len(tt.wantRevs) == 0:
// Do nothing
default:
leaves := readRevisions(t, dbc.underlying(), tt.docID)
leaves := readRevisions(t, dbc.underlying())
for i, r := range tt.wantRevs {
// allow tests to omit RevID
if r.RevID == "" {
Expand Down
2 changes: 1 addition & 1 deletion x/sqlite/get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -849,7 +849,7 @@ func TestDBGet(t *testing.T) {
id: "foo",
options: kivik.Param("atts_since", []string{"this is an invalid rev"}),
wantStatus: http.StatusBadRequest,
wantErr: `strconv.ParseInt: parsing "this is an invalid rev": invalid syntax`,
wantErr: `invalid rev format`,
}
})
tests.Add("atts_since with non-existent rev", func(t *testing.T) interface{} {
Expand Down
2 changes: 1 addition & 1 deletion x/sqlite/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func parseRev(s string) (revision, error) {
parts := strings.SplitN(s, "-", revElements)
id, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil {
return revision{}, &internal.Error{Status: http.StatusBadRequest, Err: err}
return revision{}, &internal.Error{Status: http.StatusBadRequest, Message: "invalid rev format"}
}
if len(parts) == 1 {
// A rev that contains only a number is technically valid.
Expand Down
70 changes: 70 additions & 0 deletions x/sqlite/purge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// 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 (
"context"
"fmt"
"net/http"

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

func (d *db) Purge(ctx context.Context, request map[string][]string) (*driver.PurgeResult, error) {
tx, err := d.db.Begin()
if err != nil {
return nil, err
}
defer tx.Rollback()

stmts := newStmtCache()
result := &driver.PurgeResult{}

for docID, revs := range request {
for _, rev := range revs {
r, err := parseRev(rev)
if err != nil {
return nil, err
}
_, err = d.isLeafRev(ctx, tx, docID, r.rev, r.id)
if err != nil {
switch kivik.HTTPStatus(err) {
case http.StatusNotFound, http.StatusConflict:
// Non-leaf rev, do nothing
continue
default:
return nil, err
}
}

stmt, err := stmts.prepare(ctx, tx, d.query(`
DELETE FROM {{ .Revs }}
WHERE id = $1 AND rev = $2 AND rev_id = $3
`))
if err != nil {
return nil, err
}
_, err = stmt.ExecContext(ctx, docID, r.rev, r.id)
if err != nil {
return nil, fmt.Errorf("exec failed: %w", err)
}
if result.Purged == nil {
result.Purged = map[string][]string{}
}
result.Purged[docID] = append(result.Purged[docID], rev)
}
}

return result, tx.Commit()
}
164 changes: 164 additions & 0 deletions x/sqlite/purge_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// 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"

"github.com/google/go-cmp/cmp"
"gitlab.com/flimzy/testy"

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

func TestDBPurge(t *testing.T) {
t.Parallel()
type test struct {
db *testDB
arg map[string][]string
want *driver.PurgeResult
wantErr string
wantStatus int
wantRevs []leaf
}
tests := testy.NewTable()
tests.Add("nothing to purge", test{
arg: map[string][]string{
"foo": {"1-1234", "2-5678"},
},
want: &driver.PurgeResult{},
})
tests.Add("success", func(t *testing.T) interface{} {
d := newDB(t)
rev := d.tPut("foo", map[string]string{"foo": "bar"})

return test{
db: d,
arg: map[string][]string{
"foo": {rev},
},
want: &driver.PurgeResult{
Purged: map[string][]string{
"foo": {rev},
},
},
wantRevs: nil,
}
})
tests.Add("malformed rev", test{
arg: map[string][]string{
"foo": {"abc"},
},
wantErr: "invalid rev format",
wantStatus: http.StatusBadRequest,
})
tests.Add("attempt to purge non-leaf rev does nothing", func(t *testing.T) interface{} {
d := newDB(t)
rev := d.tPut("foo", map[string]string{"foo": "bar"})
_ = d.tPut("foo", map[string]string{"foo": "baz"}, kivik.Rev(rev))

return test{
db: d,
arg: map[string][]string{
"foo": {rev},
},
want: &driver.PurgeResult{},
wantRevs: []leaf{
{ID: "foo", Rev: 1},
{ID: "foo", Rev: 2, ParentRev: &[]int{1}[0]},
},
}
})
tests.Add("deleting conflict leaves non-conflicting leaf", func(t *testing.T) interface{} {
d := newDB(t)
_ = d.tPut("foo", map[string]interface{}{
"version": "one",
"_revisions": map[string]interface{}{
"start": 3,
"ids": []string{"ccc", "bbb", "aaa"},
},
}, kivik.Param("new_edits", false))
_ = d.tPut("foo", map[string]interface{}{
"version": "two",
"_revisions": map[string]interface{}{
"start": 3,
"ids": []string{"rrr", "qqq", "aaa"},
},
}, kivik.Param("new_edits", false))

return test{
db: d,
arg: map[string][]string{
"foo": {"3-ccc"},
},
want: &driver.PurgeResult{
Purged: map[string][]string{
"foo": {"3-ccc"},
},
},
wantRevs: []leaf{
{ID: "foo", Rev: 1, RevID: "aaa"},
{ID: "foo", Rev: 2, RevID: "bbb", ParentRev: &[]int{1}[0], ParentRevID: &[]string{"aaa"}[0]},
{ID: "foo", Rev: 2, RevID: "qqq", ParentRev: &[]int{1}[0], ParentRevID: &[]string{"aaa"}[0]},
{ID: "foo", Rev: 3, RevID: "rrr", ParentRev: &[]int{2}[0], ParentRevID: &[]string{"qqq"}[0]},
},
}
})

/*
TODO:
- What happens when purging a leaf, and its parent at the same time?
- what is purge seq?
- refactor: bulk delete, bulk lookup
*/

tests.Run(t, func(t *testing.T, tt test) {
t.Parallel()
db := tt.db
if db == nil {
db = newDB(t)
}
got, err := db.Purge(context.Background(), tt.arg)
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
}
if d := cmp.Diff(got, tt.want); d != "" {
t.Errorf("Unexpected result:\n%s", d)
}
leaves := readRevisions(t, db.underlying())
for i, r := range tt.wantRevs {
// allow tests to omit RevID
if r.RevID == "" {
leaves[i].RevID = ""
}
if r.ParentRevID == nil {
leaves[i].ParentRevID = nil
}
}
if d := cmp.Diff(tt.wantRevs, leaves); d != "" {
t.Errorf("Unexpected leaves: %s", d)
}
})
}
2 changes: 1 addition & 1 deletion x/sqlite/put_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1134,7 +1134,7 @@ func TestDBPut(t *testing.T) {
if len(tt.wantRevs) == 0 {
t.Errorf("No leaves to check")
}
leaves := readRevisions(t, dbc.underlying(), tt.docID)
leaves := readRevisions(t, dbc.underlying())
for i, r := range tt.wantRevs {
// allow tests to omit RevID
if r.RevID == "" {
Expand Down
2 changes: 1 addition & 1 deletion x/sqlite/putattachment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ func TestDBPutAttachment(t *testing.T) {
if len(tt.wantRevs) == 0 {
t.Errorf("No leaves to check")
}
leaves := readRevisions(t, dbc.underlying(), tt.docID)
leaves := readRevisions(t, dbc.underlying())
for i, r := range tt.wantRevs {
// allow tests to omit RevID
if r.RevID == "" {
Expand Down
Loading

0 comments on commit 388f4b0

Please sign in to comment.