Skip to content

Commit

Permalink
Merge pull request #884 from go-kivik/revisions
Browse files Browse the repository at this point in the history
Begin support for new_edits=false & _revisions
  • Loading branch information
flimzy authored Feb 15, 2024
2 parents 62d8c26 + 0954825 commit d888232
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 30 deletions.
61 changes: 44 additions & 17 deletions x/sqlite/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,24 +71,51 @@ func (d *db) Put(ctx context.Context, docID string, doc interface{}, options dri
defer tx.Rollback()

if newEdits, _ := opts["new_edits"].(bool); !newEdits {
if docRev == "" {
return "", &internal.Error{Status: http.StatusBadRequest, Message: "When `new_edits: false`, the document needs `_rev` or `_revisions` specified"}
}
rev, err := parseRev(docRev)
if err != nil {
return "", err
}
_, err = tx.ExecContext(ctx, fmt.Sprintf(`
var rev revision
if data.Revisions.Start != 0 {
stmt, err := tx.PrepareContext(ctx, fmt.Sprintf(`
INSERT INTO %[1]q (id, rev, rev_id, parent_rev, parent_rev_id)
VALUES ($1, $2, $3, $4, $5)
`, d.name+"_revs"))
if err != nil {
return "", err
}
defer stmt.Close()

var (
parentRev *int
parentRevID *string
)
for _, r := range data.Revisions.revs() {
r := r
_, err := stmt.ExecContext(ctx, data.ID, r.rev, r.id, parentRev, parentRevID)
if err != nil {
return "", err
}
parentRev = &r.rev
parentRevID = &r.id
}
rev = data.Revisions.leaf()
} else {
if docRev == "" {
return "", &internal.Error{Status: http.StatusBadRequest, Message: "When `new_edits: false`, the document needs `_rev` or `_revisions` specified"}
}
rev, err = parseRev(docRev)
if err != nil {
return "", err
}
_, err = tx.ExecContext(ctx, fmt.Sprintf(`
INSERT INTO %[1]q (id, rev, rev_id)
VALUES ($1, $2, $3)
`, d.name+"_revs"), docID, rev.rev, rev.id)
if err != nil {
var sqliteErr *sqlite.Error
// A conflict here can be ignored, as we're not actually writing the
// document, and the rev can theoretically be updated without the doc
// during replication.
if !errors.As(err, &sqliteErr) || sqliteErr.Code() != sqlite3.SQLITE_CONSTRAINT_UNIQUE {
return "", err
if err != nil {
var sqliteErr *sqlite.Error
// A conflict here can be ignored, as we're not actually writing the
// document, and the rev can theoretically be updated without the doc
// during replication.
if !errors.As(err, &sqliteErr) || sqliteErr.Code() != sqlite3.SQLITE_CONSTRAINT_UNIQUE {
return "", err
}
}
}
var newRev string
Expand Down Expand Up @@ -139,7 +166,7 @@ func (d *db) Put(ctx context.Context, docID string, doc interface{}, options dri
FROM %[1]q
WHERE id = $1
RETURNING rev, rev_id
`, d.name+"_revs"), data.ID, data.Rev, curRevRev, curRevID).Scan(&r.rev, &r.id)
`, d.name+"_revs"), data.ID, data.RevID, curRevRev, curRevID).Scan(&r.rev, &r.id)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -387,7 +414,7 @@ func (d *db) Delete(ctx context.Context, docID string, options driver.Options) (
FROM %[1]q
WHERE id = $1
RETURNING rev, rev_id
`, d.name+"_revs"), data.ID, data.Rev, curRevRev, curRevID).Scan(&r.rev, &r.id)
`, d.name+"_revs"), data.ID, data.RevID, curRevRev, curRevID).Scan(&r.rev, &r.id)
if err != nil {
return "", err
}
Expand Down
41 changes: 41 additions & 0 deletions x/sqlite/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,47 @@ func TestDBPut(t *testing.T) {
}
},
},
{
name: "new_edits=false, with _revisions",
docID: "foo",
doc: map[string]interface{}{
"_revisions": map[string]interface{}{
"ids": []string{"ghi", "def", "abc"},
"start": 3,
},
"foo": "bar",
},
options: kivik.Param("new_edits", false),
wantRev: "3-ghi",
wantRevs: []leaf{
{
ID: "foo",
Rev: 1,
RevID: "abc",
},
{
ID: "foo",
Rev: 2,
RevID: "def",
ParentRev: &[]int{1}[0],
ParentRevID: &[]string{"abc"}[0],
},
{
ID: "foo",
Rev: 3,
RevID: "ghi",
ParentRev: &[]int{2}[0],
ParentRevID: &[]string{"def"}[0],
},
},
},
/*
- _revisions and _rev conflict
- _revisions and rev query parameter conflict
- _revisions replay
- _revisions partial replay (some revs already exist)
- _revisions with some revs and docs already exist
*/
}

for _, tt := range tests {
Expand Down
31 changes: 26 additions & 5 deletions x/sqlite/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,31 @@ func parseRev(s string) (revision, error) {

// docData represents the document id, rev, deleted status, etc.
type docData struct {
ID string `json:"_id"`
Rev string `json:"_rev"`
Deleted bool `json:"_deleted"`
Doc []byte
ID string `json:"_id"`
// RevID is the calculated revision ID, not the actual _rev field from the
// document.
RevID string `json:"-"`
Revisions revsInfo `json:"_revisions"`
Deleted bool `json:"_deleted"`
Doc []byte
}

type revsInfo struct {
Start int `json:"start"`
IDs []string `json:"ids"`
}

func (r *revsInfo) revs() []revision {
revs := make([]revision, len(r.IDs))
for i, id := range r.IDs {
revs[len(r.IDs)-i-1] = revision{rev: r.Start - i, id: id}
}
return revs
}

// leaf returns the leaf revision of the revsInfo.
func (r *revsInfo) leaf() revision {
return revision{rev: r.Start, id: r.IDs[0]}
}

// prepareDoc prepares the doc for insertion. It returns the new docID, rev, and
Expand Down Expand Up @@ -93,7 +114,7 @@ func prepareDoc(docID string, doc interface{}) (*docData, error) {
if _, err := io.Copy(h, bytes.NewReader(b)); err != nil {
return nil, err
}
data.Rev = hex.EncodeToString(h.Sum(nil))
data.RevID = hex.EncodeToString(h.Sum(nil))
data.Doc = b
return data, nil
}
Expand Down
64 changes: 56 additions & 8 deletions x/sqlite/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ func Test_prepareDoc(t *testing.T) {
name: "no rev in document",
doc: map[string]string{"foo": "bar"},
want: &docData{
Rev: "9bb58f26192e4ba00f01e2e7b136bbd8",
Doc: []byte(`{"foo":"bar"}`),
RevID: "9bb58f26192e4ba00f01e2e7b136bbd8",
Doc: []byte(`{"foo":"bar"}`),
},
},
{
Expand All @@ -45,18 +45,18 @@ func Test_prepareDoc(t *testing.T) {
"foo": "bar",
},
want: &docData{
Rev: "9bb58f26192e4ba00f01e2e7b136bbd8",
Doc: []byte(`{"foo":"bar"}`),
RevID: "9bb58f26192e4ba00f01e2e7b136bbd8",
Doc: []byte(`{"foo":"bar"}`),
},
},
{
name: "add docID",
docID: "foo",
doc: map[string]string{"foo": "bar"},
want: &docData{
ID: "foo",
Rev: "9bb58f26192e4ba00f01e2e7b136bbd8",
Doc: []byte(`{"foo":"bar"}`),
ID: "foo",
RevID: "9bb58f26192e4ba00f01e2e7b136bbd8",
Doc: []byte(`{"foo":"bar"}`),
},
},
{
Expand All @@ -67,7 +67,7 @@ func Test_prepareDoc(t *testing.T) {
"foo": "bar",
},
want: &docData{
Rev: "6872a0fc474ada5c46ce054b92897063",
RevID: "6872a0fc474ada5c46ce054b92897063",
Doc: []byte(`{"_deleted":true,"foo":"bar"}`),
Deleted: true,
},
Expand Down Expand Up @@ -169,3 +169,51 @@ func Test_extractRev(t *testing.T) {
})
}
}

func Test_revsInfo_revs(t *testing.T) {
tests := []struct {
name string
ri revsInfo
want []string
}{
{
name: "empty",
ri: revsInfo{},
want: []string{},
},
{
name: "single",
ri: revsInfo{
Start: 1,
IDs: []string{"a"},
},
want: []string{
"1-a",
},
},
{
name: "multiple",
ri: revsInfo{
Start: 8,
IDs: []string{"z", "y", "x"},
},
want: []string{
"6-x",
"7-y",
"8-z",
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := []string{}
for _, r := range tt.ri.revs() {
got = append(got, r.String())
}
if d := cmp.Diff(tt.want, got); d != "" {
t.Errorf(d)
}
})
}
}

0 comments on commit d888232

Please sign in to comment.