Skip to content

Commit

Permalink
Merge pull request #1034 from go-kivik/missingDB
Browse files Browse the repository at this point in the history
x/sqlite: Handle DB not found errors
  • Loading branch information
flimzy authored Jul 23, 2024
2 parents 958ade9 + 764e41e commit 3d100d1
Show file tree
Hide file tree
Showing 17 changed files with 232 additions and 19 deletions.
2 changes: 1 addition & 1 deletion pouchdb/find_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ func TestExplain(t *testing.T) {
tests.Add("query error", test{
db: &db{db: bindings.GlobalPouchDB().New("foo", nil)},
query: nil,
err: "TypeError: Cannot read properties",
err: "TypeError: Cannot read propert",
})
tests.Add("simple selector", func(t *testing.T) interface{} {
options := map[string]interface{}{
Expand Down
2 changes: 1 addition & 1 deletion x/sqlite/changes.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ func (d *db) newNormalChanges(ctx context.Context, opts optsMap, since, lastSeq

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

// The first row is used to calculate the ETag; it's done as part of the
Expand Down
2 changes: 1 addition & 1 deletion x/sqlite/createdoc.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func (d *db) CreateDoc(ctx context.Context, doc interface{}, _ driver.Options) (
)
`), data.ID).Scan(&exists)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return "", "", err
return "", "", d.errDatabaseNotFound(err)
}
if exists {
return "", "", &kerrors.Error{Status: http.StatusConflict, Message: "document update conflict"}
Expand Down
1 change: 1 addition & 0 deletions x/sqlite/createdoc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ func TestDBCreateDoc(t *testing.T) {
})
/*
TODO:
- nil doc
- different UUID configuration options????
- retry in case of duplicate random uuid ???
- support batch mode?
Expand Down
19 changes: 19 additions & 0 deletions x/sqlite/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,12 @@ package sqlite
import (
"context"
"database/sql"
"fmt"
"log"
"net/http"

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

type db struct {
Expand All @@ -43,6 +46,7 @@ func (d *db) Close() error {
return d.db.Close()
}

// TODO: I think Ping belongs on *client, not *db
func (d *db) Ping(ctx context.Context) error {
return d.db.PingContext(ctx)
}
Expand Down Expand Up @@ -88,3 +92,18 @@ func (db) DeleteIndex(context.Context, string, string, driver.Options) error {
func (db) Explain(context.Context, interface{}, driver.Options) (*driver.QueryPlan, error) {
return nil, nil
}

// errDatabaseNotFound converts a sqlite "no such table" error into a kivik
// database not found error
func (d *db) errDatabaseNotFound(err error) error {
if err == nil {
return nil
}
if errIsNoSuchTable(err) {
return &internal.Error{
Status: http.StatusNotFound,
Message: fmt.Sprintf("database not found: %s", d.name),
}
}
return err
}
193 changes: 193 additions & 0 deletions x/sqlite/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,17 @@
package sqlite

import (
"context"
"database/sql"
"encoding/json"
"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/int/mock"
)

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

func Test_db_not_found(t *testing.T) {
t.Parallel()
const (
wantStatus = http.StatusNotFound
wantErr = "database not found: db_not_found"
)

type test struct {
call func(*db) error
}

tests := testy.NewTable()
tests.Add("Changes", test{
call: func(d *db) error {
_, err := d.Changes(context.Background(), mock.NilOption)
return err
},
})
tests.Add("Changes, since=now", test{
call: func(d *db) error {
_, err := d.Changes(context.Background(), kivik.Param("since", "now"))
return err
},
})
tests.Add("Changes, longpoll", test{
call: func(d *db) error {
_, err := d.Changes(context.Background(), kivik.Param("feed", "longpoll"))
return err
},
})
tests.Add("CreateDoc", test{
call: func(d *db) error {
_, _, err := d.CreateDoc(context.Background(), map[string]string{}, mock.NilOption)
return err
},
})
tests.Add("DeleteAttachment", test{
call: func(d *db) error {
_, err := d.DeleteAttachment(context.Background(), "doc", "att", kivik.Rev("1-x"))
return err
},
})
tests.Add("Delete", test{
call: func(d *db) error {
_, err := d.Delete(context.Background(), "doc", kivik.Rev("1-x"))
return err
},
})
tests.Add("Find", test{
call: func(d *db) error {
_, err := d.Find(context.Background(), json.RawMessage(`{"selector":{}}`), mock.NilOption)
return err
},
})
tests.Add("GetAttachment", test{
call: func(d *db) error {
_, err := d.GetAttachment(context.Background(), "doc", "att", mock.NilOption)
return err
},
})
tests.Add("GetAttachmentMeta", test{
call: func(d *db) error {
_, err := d.GetAttachmentMeta(context.Background(), "doc", "att", mock.NilOption)
return err
},
})
tests.Add("Get", test{
call: func(d *db) error {
_, err := d.Get(context.Background(), "doc", mock.NilOption)
return err
},
})
tests.Add("GetRev", test{
call: func(d *db) error {
_, err := d.GetRev(context.Background(), "doc", mock.NilOption)
return err
},
})
tests.Add("OpenRevs", test{
call: func(d *db) error {
_, err := d.OpenRevs(context.Background(), "doc", nil, mock.NilOption)
return err
},
})
tests.Add("Purge", test{
call: func(d *db) error {
_, err := d.Purge(context.Background(), map[string][]string{"doc": {"1-x"}})
return err
},
})
tests.Add("PutAttachment", test{
call: func(d *db) error {
_, err := d.PutAttachment(context.Background(), "doc", &driver.Attachment{}, mock.NilOption)
return err
},
})
tests.Add("Put", test{
call: func(d *db) error {
_, err := d.Put(context.Background(), "doc", map[string]string{}, mock.NilOption)
return err
},
})
tests.Add("Put, new_edits=false", test{
call: func(d *db) error {
_, err := d.Put(context.Background(), "doc", map[string]any{
"_rev": "1-x",
}, kivik.Param("new_edits", false))
return err
},
})
tests.Add("Put, new_edits=false + _revisions", test{
call: func(d *db) error {
_, err := d.Put(context.Background(), "doc", map[string]any{
"_revisions": map[string]interface{}{
"start": 1,
"ids": []string{"x"},
},
}, kivik.Param("new_edits", false))
return err
},
})
tests.Add("Put, _revisoins + new_edits=true", test{
call: func(d *db) error {
_, err := d.Put(context.Background(), "doc", map[string]any{
"_revisions": map[string]interface{}{
"start": 1,
"ids": []string{"x", "y", "z"},
},
}, mock.NilOption)
return err
},
})
tests.Add("AllDocs", test{
call: func(d *db) error {
_, err := d.AllDocs(context.Background(), mock.NilOption)
return err
},
})
tests.Add("Query", test{
call: func(d *db) error {
_, err := d.Query(context.Background(), "ddoc", "view", mock.NilOption)
return err
},
})
tests.Add("Query, group=true", test{
call: func(d *db) error {
_, err := d.Query(context.Background(), "ddoc", "view", kivik.Param("group", true))
return err
},
})
tests.Add("RevsDiff", test{
call: func(d *db) error {
_, err := d.RevsDiff(context.Background(), map[string][]string{"doc": {"1-x"}})
return err
},
})
/*
TODO:
*/

tests.Run(t, func(t *testing.T, tt test) {
t.Parallel()
client, err := (drv{}).NewClient(":memory:", mock.NilOption)
if err != nil {
t.Fatal(err)
}
d, err := client.DB("db_not_found", mock.NilOption)
if err != nil {
t.Fatal(err)
}

err = tt.call(d.(*db))
if err == nil {
t.Fatal("Expected error")
}
if wantErr != err.Error() {
t.Errorf("Unexpected error: %s", err)
}
if status := kivik.HTTPStatus(err); status != wantStatus {
t.Errorf("Unexpected status: %d", status)
}
})
}
2 changes: 1 addition & 1 deletion x/sqlite/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func (d *db) Delete(ctx context.Context, docID string, options driver.Options) (

data.MD5sum, err = d.isLeafRev(ctx, tx, docID, curRev.rev, curRev.id)
if err != nil {
return "", err
return "", d.errDatabaseNotFound(err)
}
data.Deleted = true
data.Doc = []byte("{}")
Expand Down
2 changes: 1 addition & 1 deletion x/sqlite/deleteattachment.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func (d *db) DeleteAttachment(ctx context.Context, docID, filename string, optio

data.MD5sum, err = d.isLeafRev(ctx, tx, docID, curRev.rev, curRev.id)
if err != nil {
return "", err
return "", d.errDatabaseNotFound(err)
}

// Read list of current attachments, then remove the requested one
Expand Down
2 changes: 1 addition & 1 deletion x/sqlite/getrev.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func (d *db) getCoreDoc(ctx context.Context, tx queryer, id string, rev revision
case errors.Is(err, sql.ErrNoRows) || deleted && rev.IsZero():
return nil, revision{}, &internal.Error{Status: http.StatusNotFound, Message: "not found"}
case err != nil:
return nil, revision{}, err
return nil, revision{}, d.errDatabaseNotFound(err)
}

return &fullDoc{
Expand Down
2 changes: 1 addition & 1 deletion x/sqlite/openrevs.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ func (d *db) OpenRevs(ctx context.Context, docID string, revs []string, options
`), strings.Join(values, ", "))
rows, err := d.db.QueryContext(ctx, query, args...) //nolint:rowserrcheck // Err checked in Next
if err != nil {
return nil, err
return nil, d.errDatabaseNotFound(err)
}

// Call rows.Next() to see if we get any results at all. If zero results,
Expand Down
2 changes: 1 addition & 1 deletion x/sqlite/purge.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func (d *db) Purge(ctx context.Context, request map[string][]string) (*driver.Pu
// Non-leaf rev, do nothing
continue
default:
return nil, err
return nil, d.errDatabaseNotFound(err)
}
}

Expand Down
8 changes: 4 additions & 4 deletions x/sqlite/put.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func (d *db) Put(ctx context.Context, docID string, doc interface{}, options dri
for _, r := range revs[:len(revs)-1] {
err := stmt.QueryRowContext(ctx, data.ID, r.rev, r.id).Scan(&exists)
if err != nil {
return "", err
return "", d.errDatabaseNotFound(err)
}
if !exists {
return "", &internal.Error{Status: http.StatusConflict, Message: "document update conflict"}
Expand Down Expand Up @@ -101,7 +101,7 @@ func (d *db) Put(ctx context.Context, docID string, doc interface{}, options dri
r := r
_, err := stmt.ExecContext(ctx, data.ID, r.rev, r.id, parentRev, parentRevID)
if err != nil {
return "", err
return "", d.errDatabaseNotFound(err)
}
parentRev = &r.rev
parentRevID = &r.id
Expand All @@ -121,7 +121,7 @@ func (d *db) Put(ctx context.Context, docID string, doc interface{}, options dri
ON CONFLICT DO NOTHING
`), docID, rev.rev, rev.id)
if err != nil {
return "", err
return "", d.errDatabaseNotFound(err)
}
}
var newRev string
Expand Down Expand Up @@ -163,7 +163,7 @@ func (d *db) Put(ctx context.Context, docID string, doc interface{}, options dri
return "", &internal.Error{Status: http.StatusConflict, Message: "document update conflict"}
}
case err != nil:
return "", err
return "", d.errDatabaseNotFound(err)
}

r, err := d.createRev(ctx, tx, data, curRev)
Expand Down
2 changes: 1 addition & 1 deletion x/sqlite/putattachment.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func (d *db) PutAttachment(ctx context.Context, docID string, att *driver.Attach
// This means the doc is being created, and is empty other than the attachment
data.Doc = []byte("{}")
case err != nil:
return "", err
return "", d.errDatabaseNotFound(err)
}

content, err := io.ReadAll(att.Content)
Expand Down
4 changes: 2 additions & 2 deletions x/sqlite/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func (d *db) performQuery(
for {
rev, err := d.updateIndex(ctx, ddoc, view, vopts.update)
if err != nil {
return nil, err
return nil, d.errDatabaseNotFound(err)
}

args := []interface{}{
Expand Down Expand Up @@ -304,7 +304,7 @@ func (d *db) performGroupQuery(ctx context.Context, ddoc, view string, vopts *vi
for {
rev, err = d.updateIndex(ctx, ddoc, view, vopts.update)
if err != nil {
return nil, err
return nil, d.errDatabaseNotFound(err)
}

args := []any{"_design/" + ddoc, rev.rev, rev.id, view, kivik.EndKeySuffix, true, vopts.updateSeq}
Expand Down
2 changes: 1 addition & 1 deletion x/sqlite/revsdiff.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func (d *db) RevsDiff(ctx context.Context, revMap interface{}) (driver.Rows, err

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

return &revsDiffResponse{
Expand Down
Loading

0 comments on commit 3d100d1

Please sign in to comment.