Skip to content

Commit

Permalink
Merge pull request #889 from go-kivik/deleteLosingLeaf
Browse files Browse the repository at this point in the history
Delete losing leaf properly
  • Loading branch information
flimzy authored Feb 19, 2024
2 parents 04c8c69 + 9d497d8 commit cbe4d27
Show file tree
Hide file tree
Showing 4 changed files with 275 additions and 200 deletions.
66 changes: 0 additions & 66 deletions x/sqlite/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,8 @@ package sqlite
import (
"context"
"database/sql"
"errors"
"fmt"
"net/http"

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

type db struct {
Expand All @@ -38,68 +34,6 @@ func (db) CreateDoc(context.Context, interface{}, driver.Options) (string, strin
return "", "", nil
}

func (d *db) Delete(ctx context.Context, docID string, options driver.Options) (string, error) {
opts := map[string]interface{}{}
options.Apply(opts)
docRev, _ := opts["rev"].(string)

data, err := prepareDoc(docID, map[string]interface{}{"_deleted": true})
if err != nil {
return "", err
}

tx, err := d.db.BeginTx(ctx, nil)
if err != nil {
return "", err
}
defer tx.Rollback()

var curRev revision
err = tx.QueryRowContext(ctx, fmt.Sprintf(`
SELECT rev, rev_id
FROM %q
WHERE id = $1
ORDER BY rev DESC, rev_id DESC
LIMIT 1
`, d.name), data.ID).Scan(&curRev.rev, &curRev.id)
switch {
case errors.Is(err, sql.ErrNoRows):
return "", &internal.Error{Status: http.StatusNotFound, Message: "not found"}
case err != nil:
return "", err
}
if curRev.String() != docRev {
return "", &internal.Error{Status: http.StatusConflict, Message: "conflict"}
}
var (
r revision
curRevRev *int
curRevID *string
)
if curRev.rev != 0 {
curRevRev = &curRev.rev
curRevID = &curRev.id
}
err = tx.QueryRowContext(ctx, fmt.Sprintf(`
INSERT INTO %[1]q (id, rev, rev_id, parent_rev, parent_rev_id)
SELECT $1, COALESCE(MAX(rev),0) + 1, $2, $3, $4
FROM %[1]q
WHERE id = $1
RETURNING rev, rev_id
`, d.name+"_revs"), data.ID, data.RevID, curRevRev, curRevID).Scan(&r.rev, &r.id)
if err != nil {
return "", err
}
_, err = tx.ExecContext(ctx, fmt.Sprintf(`
INSERT INTO %[1]q (id, rev, rev_id, doc, deleted)
VALUES ($1, $2, $3, $4, TRUE)
`, d.name), data.ID, r.rev, r.id, data.Doc)
if err != nil {
return "", err
}
return r.String(), tx.Commit()
}

func (db) Stats(context.Context) (*driver.DBStats, error) {
return nil, nil
}
Expand Down
134 changes: 0 additions & 134 deletions x/sqlite/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,8 @@
package sqlite

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

"gitlab.com/flimzy/testy"

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

type leaf struct {
Expand Down Expand Up @@ -61,129 +53,3 @@ func readRevisions(t *testing.T, db *sql.DB, id string) []leaf {
}
return leaves
}

func TestDBDelete(t *testing.T) {
t.Parallel()
tests := []struct {
name string
setup func(*testing.T, driver.DB)
id string
options driver.Options
wantRev string
check func(*testing.T, driver.DB)
wantStatus int
wantErr string
}{
{
name: "not found",
id: "foo",
wantStatus: http.StatusNotFound,
wantErr: "not found",
},
{
name: "success",
setup: func(t *testing.T, d driver.DB) {
_, err := d.Put(context.Background(), "foo", map[string]string{"foo": "bar"}, mock.NilOption)
if err != nil {
t.Fatal(err)
}
},
id: "foo",
options: kivik.Rev("1-9bb58f26192e4ba00f01e2e7b136bbd8"),
wantRev: "2-df2a4fe30cde39c357c8d1105748d1b9",
check: func(t *testing.T, d driver.DB) {
var deleted bool
err := d.(*db).db.QueryRow(`
SELECT deleted
FROM test
WHERE id='foo'
ORDER BY rev DESC, rev_id DESC
LIMIT 1
`).Scan(&deleted)
if err != nil {
t.Fatal(err)
}
if !deleted {
t.Errorf("Document not marked deleted")
}
},
},
{
name: "replay delete should conflict",
setup: func(t *testing.T, d driver.DB) {
rev, err := d.Put(context.Background(), "foo", map[string]string{"foo": "bar"}, mock.NilOption)
if err != nil {
t.Fatal(err)
}
_, err = d.Delete(context.Background(), "foo", kivik.Rev(rev))
if err != nil {
t.Fatal(err)
}
},
id: "foo",
options: kivik.Rev("1-9bb58f26192e4ba00f01e2e7b136bbd8"),
wantStatus: http.StatusConflict,
wantErr: "conflict",
},
{
name: "delete deleted doc should succeed",
setup: func(t *testing.T, d driver.DB) {
rev, err := d.Put(context.Background(), "foo", map[string]string{"foo": "bar"}, mock.NilOption)
if err != nil {
t.Fatal(err)
}
_, err = d.Delete(context.Background(), "foo", kivik.Rev(rev))
if err != nil {
t.Fatal(err)
}
},
id: "foo",
options: kivik.Rev("2-df2a4fe30cde39c357c8d1105748d1b9"),
wantRev: "3-df2a4fe30cde39c357c8d1105748d1b9",
},
{
name: "delete without rev",
setup: func(t *testing.T, d driver.DB) {
_, err := d.Put(context.Background(), "foo", map[string]string{"foo": "bar"}, mock.NilOption)
if err != nil {
t.Fatal(err)
}
},
id: "foo",
wantStatus: http.StatusConflict,
wantErr: "conflict",
},
/* _revisions */
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
db := newDB(t)
if tt.setup != nil {
tt.setup(t, db)
}
opts := tt.options
if opts == nil {
opts = mock.NilOption
}
rev, err := db.Delete(context.Background(), tt.id, opts)
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 rev != tt.wantRev {
t.Errorf("Unexpected rev: %s", rev)
}
if tt.check != nil {
tt.check(t, db)
}
})
}
}
89 changes: 89 additions & 0 deletions x/sqlite/delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// 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://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"
"database/sql"
"errors"
"fmt"
"net/http"

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

func (d *db) Delete(ctx context.Context, docID string, options driver.Options) (string, error) {
opts := map[string]interface{}{}
options.Apply(opts)
optRev, ok := opts["rev"].(string)
if !ok {
// Special case: No rev for DELETE is always a conflict, since you can't
// delete a doc without a rev.
return "", &internal.Error{Status: http.StatusConflict, Message: "conflict"}
}
delRev, err := parseRev(optRev)
if err != nil {
return "", err
}

data, err := prepareDoc(docID, map[string]interface{}{"_deleted": true})
if err != nil {
return "", err
}

tx, err := d.db.BeginTx(ctx, nil)
if err != nil {
return "", err
}
defer tx.Rollback()

var found bool
err = tx.QueryRowContext(ctx, fmt.Sprintf(`
SELECT child.id IS NULL
FROM %[2]q AS rev
LEFT JOIN %[2]q AS child ON rev.id = child.id AND rev.rev = child.parent_rev AND rev.rev_id = child.parent_rev_id
JOIN %[1]q AS doc ON rev.id = doc.id AND rev.rev = doc.rev AND rev.rev_id = doc.rev_id
WHERE rev.id = $1
AND rev.rev = $2
AND rev.rev_id = $3
`, d.name, d.name+"_revs"), data.ID, delRev.rev, delRev.id).Scan(&found)
switch {
case errors.Is(err, sql.ErrNoRows):
return "", &internal.Error{Status: http.StatusNotFound, Message: "not found"}
case err != nil:
return "", err
}
if !found {
return "", &internal.Error{Status: http.StatusConflict, Message: "conflict"}
}
var r revision
err = tx.QueryRowContext(ctx, fmt.Sprintf(`
INSERT INTO %[1]q (id, rev, rev_id, parent_rev, parent_rev_id)
SELECT $1, COALESCE(MAX(rev),0) + 1, $2, $3, $4
FROM %[1]q
WHERE id = $1
RETURNING rev, rev_id
`, d.name+"_revs"), data.ID, data.RevID, delRev.rev, delRev.id).Scan(&r.rev, &r.id)
if err != nil {
return "", err
}
_, err = tx.ExecContext(ctx, fmt.Sprintf(`
INSERT INTO %[1]q (id, rev, rev_id, doc, deleted)
VALUES ($1, $2, $3, $4, TRUE)
`, d.name), data.ID, r.rev, r.id, data.Doc)
if err != nil {
return "", err
}
return r.String(), tx.Commit()
}
Loading

0 comments on commit cbe4d27

Please sign in to comment.