Skip to content

Commit

Permalink
Merge pull request #927 from go-kivik/replicatingAttachments
Browse files Browse the repository at this point in the history
Replicating attachments
  • Loading branch information
flimzy committed Apr 8, 2024
2 parents 3744e05 + be60a90 commit 13291c1
Show file tree
Hide file tree
Showing 4 changed files with 146 additions and 22 deletions.
1 change: 1 addition & 0 deletions x/sqlite/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ func (a *attachment) calculate(filename string) error {
return nil
}

// revs returns the revision list in oldest first order.
func (r *revsInfo) revs() []revision {
revs := make([]revision, len(r.IDs))
for i, id := range r.IDs {
Expand Down
15 changes: 11 additions & 4 deletions x/sqlite/put.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ func (d *db) Put(ctx context.Context, docID string, doc interface{}, options dri

if !newEdits { // new_edits=false means replication mode
var rev revision
var ancestorRev *revision
if data.Revisions.Start != 0 {
ancestorRev = &data.Revisions.revs()[0]
stmt, err := tx.PrepareContext(ctx, d.query(`
INSERT INTO {{ .Revs }} (id, rev, rev_id, parent_rev, parent_rev_id)
VALUES ($1, $2, $3, $4, $5)
Expand Down Expand Up @@ -114,10 +116,10 @@ func (d *db) Put(ctx context.Context, docID string, doc interface{}, options dri
return "", err
}
_, err = tx.ExecContext(ctx, d.query(`
INSERT INTO {{ .Revs }} (id, rev, rev_id)
VALUES ($1, $2, $3)
ON CONFLICT DO NOTHING
`), docID, rev.rev, rev.id)
INSERT INTO {{ .Revs }} (id, rev, rev_id)
VALUES ($1, $2, $3)
ON CONFLICT DO NOTHING
`), docID, rev.rev, rev.id)
if err != nil {
return "", err
}
Expand All @@ -138,6 +140,11 @@ func (d *db) Put(ctx context.Context, docID string, doc interface{}, options dri
if err != nil {
return "", err
}

if err := createDocAttachments(ctx, data, tx, d, rev, ancestorRev); err != nil {
return "", err
}

return newRev, tx.Commit()
}

Expand Down
125 changes: 117 additions & 8 deletions x/sqlite/put_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -915,7 +915,7 @@ func TestDBPut(t *testing.T) {
"_attachments": newAttachments().addStub("invalid.png"),
},
wantStatus: http.StatusPreconditionFailed,
wantErr: "invalid attachment stub in bar for invalid.png",
wantErr: "invalid attachment stub in foo for invalid.png",
}
})
tests.Add("update to conflicting leaf updates the proper branch", func(t *testing.T) interface{} {
Expand Down Expand Up @@ -983,17 +983,126 @@ func TestDBPut(t *testing.T) {
},
}
})
tests.Add("new_edits=false with an attachment", test{
docID: "foo",
doc: map[string]interface{}{
"_rev": "1-abc",
"_attachments": newAttachments().add("foo.txt", "This is a base64 encoding"),
"foo": "bar",
},
options: kivik.Param("new_edits", false),
wantRev: "1-.*",
wantRevs: []leaf{
{
ID: "foo",
Rev: 1,
},
},
wantAttachments: []attachmentRow{
{
DocID: "foo",
RevPos: 1,
Rev: 1,
Filename: "foo.txt",
Digest: "md5-TmfHxaRgUrE9l3tkAn4s0Q==",
},
},
})
tests.Add("new_edits=false with an attachment stub and no parent rev results in 412", test{
docID: "foo",
doc: map[string]interface{}{
"_rev": "1-abc",
"_attachments": newAttachments().addStub("foo.txt"),
"foo": "bar",
},
options: kivik.Param("new_edits", false),
wantStatus: http.StatusPreconditionFailed,
wantErr: "invalid attachment stub in foo for foo.txt",
})
tests.Add("new_edits=false with attachment stub and parent in _revisions works", func(t *testing.T) interface{} {
d := newDB(t)
rev := d.tPut("foo", map[string]interface{}{
"_attachments": newAttachments().add("foo.txt", "This is a base64 encoding"),
})

r, _ := parseRev(rev)

return test{
db: d,
docID: "foo",
doc: map[string]interface{}{
"_revisions": map[string]interface{}{
"ids": []string{"ghi", "def", r.id},
"start": 3,
},
"_attachments": newAttachments().addStub("foo.txt"),
},
options: kivik.Param("new_edits", false),
wantRev: "3-.*",
wantRevs: []leaf{
{
ID: "foo",
Rev: 1,
RevID: r.id,
},
{
ID: "foo",
Rev: 2,
RevID: "def",
ParentRev: &[]int{1}[0],
ParentRevID: &r.id,
},
{
ID: "foo",
Rev: 3,
RevID: "ghi",
ParentRev: &[]int{2}[0],
ParentRevID: &[]string{"def"}[0],
},
},
wantAttachments: []attachmentRow{
{
DocID: "foo",
RevPos: 1,
Rev: 1,
Filename: "foo.txt",
Digest: "md5-TmfHxaRgUrE9l3tkAn4s0Q==",
},
{
DocID: "foo",
RevPos: 1,
Rev: 3,
Filename: "foo.txt",
Digest: "md5-TmfHxaRgUrE9l3tkAn4s0Q==",
},
},
}
})
tests.Add("new_edits=false with attachment stub and no parent in _revisions returns 412", func(t *testing.T) interface{} {
d := newDB(t)
_ = d.tPut("foo", map[string]interface{}{
"_attachments": newAttachments().add("foo.txt", "This is a base64 encoding"),
})

return test{
db: d,
docID: "foo",
doc: map[string]interface{}{
"_revisions": map[string]interface{}{
"ids": []string{"ghi", "def"},
"start": 6,
},
"_attachments": newAttachments().addStub("foo.txt"),
},
options: kivik.Param("new_edits", false),
wantStatus: http.StatusPreconditionFailed,
wantErr: "invalid attachment stub in foo for foo.txt",
}
})

/*
TODO:
- delete attachments only in one branch of a document
- Omit attachments to delete
- Include stub to update doc without deleting attachments
- Include stub with invalid filename
- Encoding/compression?
- new_edits=false + attachment
- new_edits=false + invalid attachment stub
- filename validation?
*/

tests.Run(t, func(t *testing.T, tt test) {
Expand Down
27 changes: 17 additions & 10 deletions x/sqlite/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ func (d *db) createRev(ctx context.Context, tx *sql.Tx, data *docData, curRev re
}

// order the filenames to insert for consistency
err = createDocAttachments(ctx, data, tx, d, r, &curRev)
return r, err
}

func createDocAttachments(ctx context.Context, data *docData, tx *sql.Tx, d *db, r revision, curRev *revision) error {
orderedFilenames := make([]string, 0, len(data.Attachments))
for filename := range data.Attachments {
orderedFilenames = append(orderedFilenames, filename)
Expand All @@ -170,6 +175,9 @@ func (d *db) createRev(ctx context.Context, tx *sql.Tx, data *docData, curRev re

var pk int
if att.Stub {
if curRev == nil {
return &internal.Error{Status: http.StatusPreconditionFailed, Message: fmt.Sprintf("invalid attachment stub in %s for %s", data.ID, filename)}
}
stubStmt, err := stmts.prepare(ctx, tx, d.query(`
INSERT INTO {{ .AttachmentsBridge }} (pk, id, rev, rev_id)
SELECT att.pk, $1, $2, $3
Expand All @@ -182,18 +190,18 @@ func (d *db) createRev(ctx context.Context, tx *sql.Tx, data *docData, curRev re
RETURNING pk
`))
if err != nil {
return r, err
return err
}
err = stubStmt.QueryRowContext(ctx, data.ID, r.rev, r.id, curRev.rev, curRev.id, filename).Scan(&pk)
switch {
case errors.Is(err, sql.ErrNoRows):
return r, &internal.Error{Status: http.StatusPreconditionFailed, Message: fmt.Sprintf("invalid attachment stub in bar for %s", filename)}
return &internal.Error{Status: http.StatusPreconditionFailed, Message: fmt.Sprintf("invalid attachment stub in %s for %s", data.ID, filename)}
case err != nil:
return r, err
return err
}
} else {
if err := att.calculate(filename); err != nil {
return r, err
return err
}
contentType := att.ContentType
if contentType == "" {
Expand All @@ -206,27 +214,26 @@ func (d *db) createRev(ctx context.Context, tx *sql.Tx, data *docData, curRev re
RETURNING pk
`))
if err != nil {
return r, err
return err
}

err = attStmt.QueryRowContext(ctx, r.rev, filename, contentType, att.Length, att.Digest, att.Content).Scan(&pk)
if err != nil {
return r, err
return err
}

bridgeStmt, err := stmts.prepare(ctx, tx, d.query(`
INSERT INTO {{ .AttachmentsBridge }} (pk, id, rev, rev_id)
VALUES ($1, $2, $3, $4)
`))
if err != nil {
return r, err
return err
}
_, err = bridgeStmt.ExecContext(ctx, pk, data.ID, r.rev, r.id)
if err != nil {
return r, err
return err
}
}
}

return r, nil
return nil
}

0 comments on commit 13291c1

Please sign in to comment.