Skip to content

Commit

Permalink
Very rudamentary _conflicts support
Browse files Browse the repository at this point in the history
  • Loading branch information
flimzy committed Feb 10, 2024
1 parent 84a1104 commit 7b93e12
Show file tree
Hide file tree
Showing 3 changed files with 95 additions and 24 deletions.
64 changes: 48 additions & 16 deletions x/sqlite/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ package sqlite
import (
"context"
"database/sql"
"encoding/json"
"errors"
"io"
"net/http"
Expand Down Expand Up @@ -123,45 +124,76 @@ func (d *db) Put(ctx context.Context, docID string, doc interface{}, options dri
func (d *db) Get(ctx context.Context, id string, options driver.Options) (*driver.Document, error) {
opts := map[string]interface{}{}
options.Apply(opts)
if rev, _ := opts["rev"].(string); rev != "" {
r, err := parseRev(rev)

var rev, body string
var err error

if optsRev, _ := opts["rev"].(string); optsRev != "" {
var r revision
r, err = parseRev(optsRev)
if err != nil {
return nil, err
}
var body string
err = d.db.QueryRowContext(ctx, `
SELECT doc
FROM `+d.name+`
WHERE id = $1
AND rev_id = $2
AND rev = $3
`, id, r.id, r.rev).Scan(&body)
if errors.Is(err, sql.ErrNoRows) {
return nil, &internal.Error{Status: http.StatusNotFound, Message: "not found"}
}
if err != nil {
return nil, err
}
return &driver.Document{
Rev: rev,
Body: io.NopCloser(strings.NewReader(body)),
}, nil
}
var rev, body string
err := d.db.QueryRowContext(ctx, `
rev = optsRev
} else {
err = d.db.QueryRowContext(ctx, `
SELECT rev_id || '-' || rev, doc
FROM `+d.name+`
WHERE id = $1
AND deleted = FALSE
ORDER BY rev_id DESC, rev DESC
LIMIT 1
`, id).Scan(&rev, &body)
}

if errors.Is(err, sql.ErrNoRows) {
return nil, &internal.Error{Status: http.StatusNotFound, Message: "not found"}
}
if err != nil {
return nil, err
}

if conflicts, _ := opts["conflicts"].(bool); conflicts {
var revs []string
rows, err := d.db.QueryContext(ctx, `
SELECT rev_id || '-' || rev
FROM `+d.name+`
WHERE id = $1
AND rev_id || '-' || rev != $2
AND DELETED = FALSE
`, id, rev)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
var r string
if err := rows.Scan(&r); err != nil {
return nil, err
}
revs = append(revs, r)
}
if err := rows.Err(); err != nil {
return nil, err
}
var doc map[string]interface{}
if err := json.Unmarshal([]byte(body), &doc); err != nil {
return nil, err
}
doc["_conflicts"] = revs
jonDoc, err := json.Marshal(doc)
if err != nil {
return nil, err
}
body = string(jonDoc)
}
return &driver.Document{
Rev: rev,
Body: io.NopCloser(strings.NewReader(body)),
Expand Down
41 changes: 40 additions & 1 deletion x/sqlite/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,45 @@ func TestGet(t *testing.T) {
wantStatus: http.StatusNotFound,
wantErr: "not found",
},
{
name: "include conflicts",
setup: func(t *testing.T, d driver.DB) {
_, err := d.Put(context.Background(), "foo", map[string]string{"foo": "bar"}, kivik.Params(map[string]interface{}{
"new_edits": false,
"rev": "1-abc",
}))
if err != nil {
t.Fatal(err)
}
_, err = d.Put(context.Background(), "foo", map[string]string{"foo": "baz"}, kivik.Params(map[string]interface{}{
"new_edits": false,
"rev": "1-xyz",
}))
if err != nil {
t.Fatal(err)
}
},
id: "foo",
options: kivik.Param("conflicts", true),
wantDoc: map[string]interface{}{
"foo": "baz",
"_conflicts": []string{"1-abc"},
},
},
/*
TODO:
attachments = true
att_encoding_info = true
atts_since = [revs]
conflicts = true
deleted_conflicts = true
latest = true
local_seq = true
meta = true
open_revs = []
revs = true
revs_info = true
*/
}

for _, tt := range tests {
Expand Down Expand Up @@ -302,7 +341,7 @@ func TestGet(t *testing.T) {
t.Fatal(err)
}
if d := testy.DiffAsJSON(tt.wantDoc, gotDoc); d != nil {
t.Errorf("Unexpected doc: %s", doc)
t.Errorf("Unexpected doc: %s", d)
}
})
}
Expand Down
14 changes: 7 additions & 7 deletions x/sqlite/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,30 +25,30 @@ import (
"github.com/go-kivik/kivik/v4/internal"
)

type rev struct {
type revision struct {
id int
rev string
}

func (r rev) String() string {
func (r revision) String() string {
return strconv.Itoa(r.id) + "-" + r.rev
}

func parseRev(s string) (rev, error) {
func parseRev(s string) (revision, error) {
if s == "" {
return rev{}, &internal.Error{Status: http.StatusBadRequest, Message: "missing _rev"}
return revision{}, &internal.Error{Status: http.StatusBadRequest, Message: "missing _rev"}
}
const revElements = 2
parts := strings.SplitN(s, "-", revElements)
id, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil {
return rev{}, &internal.Error{Status: http.StatusBadRequest, Err: err}
return revision{}, &internal.Error{Status: http.StatusBadRequest, Err: err}
}
if len(parts) == 1 {
// A rev that contains only a number is technically valid.
return rev{id: int(id)}, nil
return revision{id: int(id)}, nil
}
return rev{id: int(id), rev: parts[1]}, nil
return revision{id: int(id), rev: parts[1]}, nil
}

// prepareDoc prepares the doc for insertion. It returns the new docID, rev, and
Expand Down

0 comments on commit 7b93e12

Please sign in to comment.