Skip to content

Commit

Permalink
Merge pull request #941 from go-kivik/getMeta
Browse files Browse the repository at this point in the history
Get meta
  • Loading branch information
flimzy committed Apr 16, 2024
2 parents 3d1dd76 + cb37095 commit 18d2243
Show file tree
Hide file tree
Showing 11 changed files with 642 additions and 156 deletions.
2 changes: 2 additions & 0 deletions x/sqlite/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ type DB interface {
driver.Purger
driver.RevsDiffer
driver.OpenRever
driver.AttachmentMetaGetter
driver.RevGetter
}

type testDB struct {
Expand Down
8 changes: 0 additions & 8 deletions x/sqlite/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,6 @@ func (db) BulkDocs(context.Context, []interface{}, driver.Options) ([]driver.Bul
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
}
Expand Down
122 changes: 23 additions & 99 deletions x/sqlite/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,123 +17,51 @@ import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"

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

func (d *db) Get(ctx context.Context, id string, options driver.Options) (*driver.Document, error) {
opts := map[string]interface{}{}
options.Apply(opts)

var (
r revision
body []byte
err error
deleted bool
localSeq int
)
var r revision

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

optsRev, _ := opts["rev"].(string)
latest, _ := opts["latest"].(bool)
var (
optConflicts, _ = opts["conflicts"].(bool)
optDeletedConflicts, _ = opts["deleted_conflicts"].(bool)
optRevsInfo, _ = opts["revs_info"].(bool)
optRevs, _ = opts["revs"].(bool) // TODO: opts.revs()
optLocalSeq, _ = opts["local_seq"].(bool)
optAttachments, _ = opts["attachments"].(bool)
optAttsSince, _ = opts["atts_since"].([]string)
optsRev, _ = opts["rev"].(string)
latest, _ = opts["latest"].(bool)
)

if optsRev != "" {
r, err = parseRev(optsRev)
if err != nil {
return nil, err
}
}
switch {
case optsRev != "" && !latest:
err = tx.QueryRowContext(ctx, fmt.Sprintf(`
SELECT seq, doc, deleted
FROM %q
WHERE id = $1
AND rev = $2
AND rev_id = $3
`, d.name), id, r.rev, r.id).Scan(&localSeq, &body, &deleted)
case optsRev != "" && latest:
err = tx.QueryRowContext(ctx, fmt.Sprintf(`
SELECT seq, rev, rev_id, doc, deleted FROM (
WITH RECURSIVE Descendants AS (
-- Base case: Select the starting node for descendants
SELECT id, rev, rev_id, parent_rev, parent_rev_id
FROM %[1]q AS revs
WHERE id = $1
AND rev = $2
AND rev_id = $3
UNION ALL
-- Recursive step: Select the children of the current node
SELECT r.id, r.rev, r.rev_id, r.parent_rev, r.parent_rev_id
FROM %[1]q r
JOIN Descendants d ON d.rev_id = r.parent_rev_id AND d.rev = r.parent_rev AND d.id = r.id
)
-- Combine ancestors and descendants, excluding the starting node twice
SELECT seq, rev.rev, rev.rev_id, doc, deleted
FROM Descendants AS rev
JOIN %[2]q AS doc ON doc.id = rev.id AND doc.rev = rev.rev AND doc.rev_id = rev.rev_id
LEFT JOIN %[1]q AS child ON child.parent_rev = rev.rev AND child.parent_rev_id = rev.rev_id
WHERE child.rev IS NULL
AND doc.deleted = FALSE
ORDER BY rev.rev DESC, rev.rev_id DESC
)
UNION ALL
-- This query fetches the winning non-deleted rev, in case the above
-- query returns nothing, because the latest leaf rev is deleted.
SELECT seq, rev, rev_id, doc, deleted FROM (
SELECT leaf.id, leaf.rev, leaf.rev_id, leaf.parent_rev, leaf.parent_rev_id, doc.doc, doc.deleted, doc.seq
FROM %[1]q AS leaf
LEFT JOIN %[1]q AS child ON child.id = leaf.id AND child.parent_rev = leaf.rev AND child.parent_rev_id = leaf.rev_id
JOIN %[2]q AS doc ON doc.id = leaf.id AND doc.rev = leaf.rev AND doc.rev_id = leaf.rev_id
WHERE child.rev IS NULL
AND doc.deleted = FALSE
ORDER BY leaf.rev DESC, leaf.rev_id DESC
)
LIMIT 1
`, d.name+"_revs", d.name), id, r.rev, r.id).Scan(&localSeq, &r.rev, &r.id, &body, &deleted)
default:
err = tx.QueryRowContext(ctx, fmt.Sprintf(`
SELECT seq, rev, rev_id, doc, deleted
FROM %q
WHERE id = $1
ORDER BY rev DESC, rev_id DESC
LIMIT 1
`, d.name), id).Scan(&localSeq, &r.rev, &r.id, &body, &deleted)
}

switch {
case errors.Is(err, sql.ErrNoRows) ||
deleted && optsRev == "":
return nil, &internal.Error{Status: http.StatusNotFound, Message: "not found"}
case err != nil:
toMerge, r, err := d.getCoreDoc(ctx, tx, id, r, latest, false)
if err != nil {
return nil, err
}

toMerge := fullDoc{
ID: id,
Rev: r.String(),
Deleted: deleted,
if !optLocalSeq {
toMerge.LocalSeq = 0
}

var (
optConflicts, _ = opts["conflicts"].(bool)
optDeletedConflicts, _ = opts["deleted_conflicts"].(bool)
optRevsInfo, _ = opts["revs_info"].(bool)
optRevs, _ = opts["revs"].(bool) // TODO: opts.revs()
optLocalSeq, _ = opts["local_seq"].(bool)
optAttachments, _ = opts["attachments"].(bool)
optAttsSince, _ = opts["atts_since"].([]string)
)

if meta, _ := opts["meta"].(bool); meta {
optConflicts = true
optDeletedConflicts = true
Expand Down Expand Up @@ -226,9 +154,7 @@ func (d *db) Get(ctx context.Context, id string, options driver.Options) (*drive
toMerge.Revisions = &revsInfo
}
}
if optLocalSeq {
toMerge.LocalSeq = localSeq
}

atts, err := d.getAttachments(ctx, tx, id, r, optAttachments, optAttsSince)
if err != nil {
return nil, err
Expand All @@ -237,8 +163,6 @@ func (d *db) Get(ctx context.Context, id string, options driver.Options) (*drive
toMerge.Attachments = mergeAtts
}

toMerge.Doc = body

return &driver.Document{
Attachments: atts,
Rev: r.String(),
Expand All @@ -252,21 +176,21 @@ type dbOrTx interface {

func (d *db) conflicts(ctx context.Context, tx dbOrTx, id string, r revision, deleted bool) ([]string, error) {
var revs []string
rows, err := tx.QueryContext(ctx, fmt.Sprintf(`
rows, err := tx.QueryContext(ctx, d.query(`
SELECT rev.rev || '-' || rev.rev_id
FROM %[1]q AS rev
LEFT JOIN %[1]q AS child
FROM {{ .Revs }} AS rev
LEFT JOIN {{ .Revs }} AS child
ON rev.id = child.id
AND rev.rev = child.parent_rev
AND rev.rev_id = child.parent_rev_id
JOIN %[2]q AS docs ON docs.id = rev.id
JOIN {{ .Docs }} AS docs ON docs.id = rev.id
AND docs.rev = rev.rev
AND docs.rev_id = rev.rev_id
WHERE rev.id = $1
AND NOT (rev.rev = $2 AND rev.rev_id = $3)
AND child.id IS NULL
AND docs.deleted = $4
`, d.name+"_revs", d.name), id, r.rev, r.id, deleted)
`), id, r.rev, r.id, deleted)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion x/sqlite/getattachment.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func (d *db) GetAttachment(ctx context.Context, docID string, filename string, o
return nil, &internal.Error{Message: err.Error(), Status: http.StatusBadRequest}
}
} else {
requestedRev, _, err = d.winningRev(ctx, tx, docID)
requestedRev, err = d.winningRev(ctx, tx, docID)
if errors.Is(err, sql.ErrNoRows) {
return nil, &internal.Error{Status: http.StatusNotFound, Message: "missing"}
}
Expand Down
74 changes: 31 additions & 43 deletions x/sqlite/getattachment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,59 +29,47 @@ import (
"github.com/go-kivik/kivik/v4/internal/mock"
)

type AttachmentX struct {
Filename string
ContentType string
Length int64
RevPos int64
Data string
}
type TestX struct {
db driver.DB
docID string
filename string
options driver.Options

wantAttachment *AttachmentX
wantStatus int
wantErr string
}

func TestDBGetAttachment(t *testing.T) {
t.Parallel()

type attachment struct {
Filename string
ContentType string
Length int64
RevPos int64
Data string
}

type test struct {
db driver.DB
docID string
filename string
options driver.Options

want *attachment
wantStatus int
wantErr string
}

tests := testy.NewTable()
tests.Add("document does not exist", TestX{
tests.Add("document does not exist", test{
docID: "foo",
filename: "foo.txt",
wantStatus: http.StatusNotFound,
wantErr: "missing",
})
tests.Add("when the attachment exists, return it", func(t *testing.T) interface{} {
db := newDB(t)
_ = db.tPut("foo", map[string]interface{}{
"_id": "foo",
"_attachments": newAttachments().add("foo.txt", "This is a base64 encoding"),
})

return TestX{
db: db,
docID: "foo",
filename: "foo.txt",
}
})
tests.Add("return an attachment when it exists", func(t *testing.T) interface{} {
db := newDB(t)
_ = db.tPut("foo", map[string]interface{}{
"_id": "foo",
"_attachments": newAttachments().add("foo.txt", "This is a base64 encoding"),
})

return TestX{
return test{
db: db,
docID: "foo",
filename: "foo.txt",
wantAttachment: &AttachmentX{
want: &attachment{
Filename: "foo.txt",
ContentType: "text/plain",
Length: 25,
Expand All @@ -101,7 +89,7 @@ func TestDBGetAttachment(t *testing.T) {
t.Fatal(err)
}

return TestX{
return test{
db: db,
docID: "foo",
filename: "foo.txt",
Expand All @@ -121,11 +109,11 @@ func TestDBGetAttachment(t *testing.T) {
"_attachments": newAttachments().addStub("foo.txt"),
}, kivik.Rev(rev))

return TestX{
return test{
db: db,
docID: "foo",
filename: "foo.txt",
wantAttachment: &AttachmentX{
want: &attachment{
Filename: "foo.txt",
ContentType: "text/plain",
Length: 25,
Expand All @@ -152,13 +140,13 @@ func TestDBGetAttachment(t *testing.T) {

r, _ := parseRev(rev)

return TestX{
return test{
db: d,
docID: id,
filename: filename,
options: kivik.Rev(rev),

wantAttachment: &AttachmentX{Filename: filename, ContentType: "text/plain", Length: int64(len(wantContent)), RevPos: int64(r.rev), Data: wantContent},
want: &attachment{Filename: filename, ContentType: "text/plain", Length: int64(len(wantContent)), RevPos: int64(r.rev), Data: wantContent},
}
})

Expand All @@ -179,7 +167,7 @@ func TestDBGetAttachment(t *testing.T) {
- GetAttachment returns the latest revision
*/

tests.Run(t, func(t *testing.T, tt TestX) {
tests.Run(t, func(t *testing.T, tt test) {
t.Parallel()
db := tt.db
if db == nil {
Expand All @@ -200,21 +188,21 @@ func TestDBGetAttachment(t *testing.T) {
t.Errorf("Unexpected status: %d", status)
}

if tt.wantAttachment == nil {
if tt.want == nil {
return
}
data, err := io.ReadAll(att.Content)
if err != nil {
t.Fatal(err)
}
got := &AttachmentX{
got := &attachment{
Filename: att.Filename,
ContentType: att.ContentType,
Length: att.Size,
RevPos: att.RevPos,
Data: string(data),
}
if d := cmp.Diff(tt.wantAttachment, got); d != "" {
if d := cmp.Diff(tt.want, got); d != "" {
t.Errorf("Unexpected attachment metadata:\n%s", d)
}
})
Expand Down
Loading

0 comments on commit 18d2243

Please sign in to comment.