Skip to content

Commit

Permalink
Merge pull request #898 from go-kivik/putattachment2
Browse files Browse the repository at this point in the history
Beginning of delete attachment support
  • Loading branch information
flimzy authored Mar 4, 2024
2 parents 9022b7d + bd45947 commit 6d26097
Show file tree
Hide file tree
Showing 7 changed files with 254 additions and 26 deletions.
4 changes: 0 additions & 4 deletions x/sqlite/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,6 @@ func (db) GetAttachment(context.Context, string, string, driver.Options) (*drive
return nil, nil
}

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

func (db) Query(context.Context, string, string, driver.Options) (driver.Rows, error) {
return nil, nil
}
Expand Down
18 changes: 2 additions & 16 deletions x/sqlite/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ package sqlite

import (
"context"
"database/sql"
"errors"
"fmt"
"net/http"

Expand Down Expand Up @@ -48,20 +46,8 @@ func (d *db) Delete(ctx context.Context, docID string, options driver.Options) (
}
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:
found, err := d.docRevExists(ctx, tx, docID, delRev)
if err != nil {
return "", err
}
if !found {
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 @@ -43,7 +43,7 @@ func TestDBDelete(t *testing.T) {
id: "foo",
options: kivik.Rev("1-9bb58f26192e4ba00f01e2e7b136bbd8"),
wantStatus: http.StatusNotFound,
wantErr: "not found",
wantErr: "document not found",
})
tests.Add("success", test{
setup: func(t *testing.T, d driver.DB) {
Expand Down
43 changes: 43 additions & 0 deletions x/sqlite/deleteattachment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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"
"net/http"

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

func (d *db) DeleteAttachment(ctx context.Context, docID, _ string, options driver.Options) (string, error) {
opts := newOpts(options)
if rev := opts.rev(); rev == "" {
return "", &internal.Error{Status: http.StatusConflict, Message: "conflict"}
}
tx, err := d.db.BeginTx(ctx, nil)
if err != nil {
return "", err
}
defer tx.Rollback()

rev := revision{}
found, err := d.docRevExists(ctx, tx, docID, rev)
if err != nil {
return "", err
}
if !found {
return "", &internal.Error{Status: http.StatusNotFound, Message: "document not found"}
}
return "", tx.Commit()
}
121 changes: 121 additions & 0 deletions x/sqlite/deleteattachment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// 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.

//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"
"github.com/go-kivik/kivik/v4/internal/mock"
)

func TestDBDeleteAttachment(t *testing.T) {
t.Parallel()
type test struct {
setup func(*testing.T, driver.DB)
docID string
filename string
options driver.Options
check func(*testing.T, driver.DB)
wantRev string
wantRevs []leaf
wantStatus int
wantErr string
wantAttachments []attachmentRow
}

tests := testy.NewTable()
tests.Add("doc not found", test{
docID: "foo",
filename: "foo.txt",
options: kivik.Rev("1-9bb58f26192e4ba00f01e2e7b136bbd8"),
wantErr: "document not found",
wantStatus: http.StatusNotFound,
})
tests.Add("doc exists, but no rev provided", test{
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)
}
},
docID: "foo",
filename: "foo.txt",
wantErr: "conflict",
wantStatus: http.StatusConflict,
})
tests.Add("doc exists, but wrong rev provided", test{
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)
}
},
docID: "foo",
filename: "foo.txt",
options: kivik.Rev("1-wrong"),
wantErr: "document not found",
wantStatus: http.StatusNotFound,
})

/*
TODO:
- db missing => db not found
- file does not exist => file not found
*/

tests.Run(t, func(t *testing.T, tt test) {
t.Parallel()
dbc := newDB(t)
if tt.setup != nil {
tt.setup(t, dbc)
}
opts := tt.options
if opts == nil {
opts = mock.NilOption
}
rev, err := dbc.DeleteAttachment(context.Background(), tt.docID, tt.filename, 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 tt.check != nil {
tt.check(t, dbc)
}
if err != nil {
return
}
if rev != tt.wantRev {
t.Errorf("Unexpected rev: %s, want %s", rev, tt.wantRev)
}
if len(tt.wantRevs) == 0 {
t.Errorf("No leaves to check")
}
leaves := readRevisions(t, dbc.(*db).db, tt.docID)
if d := cmp.Diff(tt.wantRevs, leaves); d != "" {
t.Errorf("Unexpected leaves: %s", d)
}
checkAttachments(t, dbc, tt.wantAttachments)
})
}
65 changes: 60 additions & 5 deletions x/sqlite/putattachment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,11 +204,66 @@ func TestDBPutAttachment(t *testing.T) {
},
},
})
/*
TODO:
- Add attachment to conflicting leaf
- Update an existing attachment
*/
tests.Add("update existing attachment", test{
setup: func(t *testing.T, db driver.DB) {
_, err := db.Put(context.Background(), "foo", map[string]interface{}{
"foo": "bar",
"_attachments": map[string]interface{}{
"foo.txt": map[string]interface{}{
"content_type": "text/plain",
"data": "SGVsbG8sIHdvcmxkIQ==",
},
},
}, mock.NilOption)
if err != nil {
t.Fatal(err)
}
},
docID: "foo",
attachment: &driver.Attachment{
Filename: "foo.txt",
ContentType: "text/plain",
Content: io.NopCloser(strings.NewReader("Hello, everybody!")),
},
options: kivik.Rev("1-53929381825df5c0a2b57f34d168999d"),
wantRev: "2-53929381825df5c0a2b57f34d168999d",
wantRevs: []leaf{
{
ID: "foo",
Rev: 1,
RevID: "53929381825df5c0a2b57f34d168999d",
},
{
ID: "foo",
Rev: 2,
RevID: "53929381825df5c0a2b57f34d168999d",
ParentRev: &[]int{1}[0],
ParentRevID: &[]string{"53929381825df5c0a2b57f34d168999d"}[0],
},
},
wantAttachments: []attachmentRow{
{
DocID: "foo",
Rev: 1,
RevID: "53929381825df5c0a2b57f34d168999d",
Filename: "foo.txt",
Digest: "md5-bNNVbesNpUvKBgtMOUeYOQ==",
Length: 13,
ContentType: "text/plain",
Data: "Hello, world!",
},
{
DocID: "foo",
Rev: 2,
RevID: "53929381825df5c0a2b57f34d168999d",
Filename: "foo.txt",
ContentType: "text/plain",
Digest: "md5-kDqL1OTtoET1YR0WdPZ5tQ==",
Length: 17,
Data: "Hello, everybody!",
},
},
})

tests.Run(t, func(t *testing.T, tt test) {
t.Parallel()
Expand Down
27 changes: 27 additions & 0 deletions x/sqlite/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,13 @@ package sqlite
import (
"context"
"database/sql"
"errors"
"fmt"
"net/http"
"sort"
"strings"

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

func placeholders(start, count int) string {
Expand Down Expand Up @@ -146,3 +150,26 @@ func (d *db) createRev(ctx context.Context, tx *sql.Tx, data *docData, curRev re

return r, nil
}

// docRevExists returns an error if the requested document does not exist. It
// returns false if the document does exist, but the specified revision is not
// the latest. It returns true, nil if both the doc and revision are valid.
func (d *db) docRevExists(ctx context.Context, tx *sql.Tx, docID string, rev revision) (bool, error) {
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"), docID, rev.rev, rev.id).Scan(&found)
switch {
case errors.Is(err, sql.ErrNoRows):
return false, &internal.Error{Status: http.StatusNotFound, Message: "document not found"}
case err != nil:
return false, err
}
return found, nil
}

0 comments on commit 6d26097

Please sign in to comment.