diff --git a/x/sqlite/db.go b/x/sqlite/db.go index 0104f3210..d15a4d4e0 100644 --- a/x/sqlite/db.go +++ b/x/sqlite/db.go @@ -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 @@ -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 } @@ -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 } diff --git a/x/sqlite/db_test.go b/x/sqlite/db_test.go index 03360d55c..3126d8e8c 100644 --- a/x/sqlite/db_test.go +++ b/x/sqlite/db_test.go @@ -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 { diff --git a/x/sqlite/json.go b/x/sqlite/json.go index f145023c1..e4d23ac9a 100644 --- a/x/sqlite/json.go +++ b/x/sqlite/json.go @@ -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 @@ -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 } diff --git a/x/sqlite/json_test.go b/x/sqlite/json_test.go index 6b1f3c9c5..b1cdf4f55 100644 --- a/x/sqlite/json_test.go +++ b/x/sqlite/json_test.go @@ -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"}`), }, }, { @@ -45,8 +45,8 @@ func Test_prepareDoc(t *testing.T) { "foo": "bar", }, want: &docData{ - Rev: "9bb58f26192e4ba00f01e2e7b136bbd8", - Doc: []byte(`{"foo":"bar"}`), + RevID: "9bb58f26192e4ba00f01e2e7b136bbd8", + Doc: []byte(`{"foo":"bar"}`), }, }, { @@ -54,9 +54,9 @@ func Test_prepareDoc(t *testing.T) { 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"}`), }, }, { @@ -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, }, @@ -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) + } + }) + } +}