Skip to content

Commit

Permalink
Merge pull request #881 from go-kivik/leafLogic
Browse files Browse the repository at this point in the history
SQLite progress
  • Loading branch information
flimzy authored Feb 12, 2024
2 parents 03eaf6c + cd6e69d commit 33d1c07
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 23 deletions.
105 changes: 88 additions & 17 deletions x/sqlite/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ func (d *db) Put(ctx context.Context, docID string, doc interface{}, options dri
if docRev == "" && optsRev != "" {
docRev = optsRev
}

docID, rev, jsonDoc, err := prepareDoc(docID, doc)
if err != nil {
return "", err
Expand Down Expand Up @@ -112,26 +111,36 @@ func (d *db) Put(ctx context.Context, docID string, doc interface{}, options dri
return newRev, tx.Commit()
}

var curRev string
var curRev revision
err = tx.QueryRowContext(ctx, fmt.Sprintf(`
SELECT COALESCE(MAX(rev || '-' || rev_id),'')
SELECT rev, rev_id
FROM %q
WHERE id = $1
`, d.name), docID).Scan(&curRev)
ORDER BY rev DESC, rev_id DESC
LIMIT 1
`, d.name), docID).Scan(&curRev.rev, &curRev.id)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return "", err
}
if curRev != docRev {
if curRev.String() != docRev {
return "", &internal.Error{Status: http.StatusConflict, Message: "conflict"}
}
var r revision
var (
r revision
curRevRev *int
curRevID *string
)
if curRev.rev != 0 {
curRevRev = &curRev.rev
curRevID = &curRev.id
}
err = tx.QueryRowContext(ctx, fmt.Sprintf(`
INSERT INTO %[1]q (id, rev, rev_id)
SELECT $1, COALESCE(MAX(rev),0) + 1, $2
INSERT INTO %[1]q (id, rev, rev_id, parent_rev, parent_rev_id)
SELECT $1, COALESCE(MAX(rev),0) + 1, $2, $3, $4
FROM %[1]q
WHERE id = $1
RETURNING rev, rev_id
`, d.name+"_revs"), docID, rev).Scan(&r.rev, &r.id)
`, d.name+"_revs"), docID, rev, curRevRev, curRevID).Scan(&r.rev, &r.id)
if err != nil {
return "", err
}
Expand Down Expand Up @@ -189,12 +198,16 @@ func (d *db) Get(ctx context.Context, id string, options driver.Options) (*drive
if conflicts, _ := opts["conflicts"].(bool); conflicts {
var revs []string
rows, err := d.db.QueryContext(ctx, fmt.Sprintf(`
SELECT rev || '-' || rev_id
FROM %q
WHERE id = $1
AND NOT (rev = $2 AND rev_id = $3)
AND DELETED = FALSE
`, d.name), id, r.rev, r.id)
SELECT rev.rev || '-' || rev.rev_id
FROM %[1]q AS rev
LEFT JOIN %[1]q AS child
ON rev.id = child.id
AND rev.rev = child.parent_rev
AND rev.rev_id = child.parent_rev_id
WHERE rev.id = $1
AND NOT (rev.rev = $2 AND rev.rev_id = $3)
AND child.id IS NULL
`, d.name+"_revs"), id, r.rev, r.id)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -226,8 +239,66 @@ func (d *db) Get(ctx context.Context, id string, options driver.Options) (*drive
}, nil
}

func (db) Delete(context.Context, string, driver.Options) (string, error) {
return "", nil
func (d *db) Delete(ctx context.Context, docID string, options driver.Options) (string, error) {
opts := map[string]interface{}{}
options.Apply(opts)
docRev, _ := opts["rev"].(string)

docID, rev, jsonDoc, err := prepareDoc(docID, map[string]interface{}{"_deleted": true})
if err != nil {
return "", err
}

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

var curRev revision
err = tx.QueryRowContext(ctx, fmt.Sprintf(`
SELECT rev, rev_id
FROM %q
WHERE id = $1
ORDER BY rev DESC, rev_id DESC
LIMIT 1
`, d.name), docID).Scan(&curRev.rev, &curRev.id)
switch {
case errors.Is(err, sql.ErrNoRows):
return "", &internal.Error{Status: http.StatusNotFound, Message: "not found"}
case err != nil:
return "", err
}
if curRev.String() != docRev {
return "", &internal.Error{Status: http.StatusConflict, Message: "conflict"}
}
var (
r revision
curRevRev *int
curRevID *string
)
if curRev.rev != 0 {
curRevRev = &curRev.rev
curRevID = &curRev.id
}
err = tx.QueryRowContext(ctx, fmt.Sprintf(`
INSERT INTO %[1]q (id, rev, rev_id, parent_rev, parent_rev_id)
SELECT $1, COALESCE(MAX(rev),0) + 1, $2, $3, $4
FROM %[1]q
WHERE id = $1
RETURNING rev, rev_id
`, d.name+"_revs"), docID, rev, curRevRev, curRevID).Scan(&r.rev, &r.id)
if err != nil {
return "", err
}
_, err = tx.ExecContext(ctx, fmt.Sprintf(`
INSERT INTO %[1]q (id, rev, rev_id, doc, deleted)
VALUES ($1, $2, $3, $4, TRUE)
`, d.name), docID, r.rev, r.id, jsonDoc)
if err != nil {
return "", err
}
return r.String(), tx.Commit()
}

func (db) Stats(context.Context) (*driver.DBStats, error) {
Expand Down
132 changes: 128 additions & 4 deletions x/sqlite/db_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,16 @@ func TestDBPut(t *testing.T) {
wantStatus: http.StatusConflict,
wantErr: "conflict",
},
{
name: "attempt to create doc with rev should conflict",
docID: "foo",
doc: map[string]interface{}{
"foo": "bar",
},
options: kivik.Rev("1-1234567890abcdef1234567890abcdef"),
wantStatus: http.StatusConflict,
wantErr: "conflict",
},
{
name: "attempt to update doc without rev should conflict",
setup: func(t *testing.T, d driver.DB) {
Expand Down Expand Up @@ -164,9 +174,11 @@ func TestDBPut(t *testing.T) {
RevID: "9bb58f26192e4ba00f01e2e7b136bbd8",
},
{
ID: "foo",
Rev: 2,
RevID: "afa7ae8a1906f4bb061be63525974f92",
ID: "foo",
Rev: 2,
RevID: "afa7ae8a1906f4bb061be63525974f92",
ParentRev: &[]int{1}[0],
ParentRevID: &[]string{"9bb58f26192e4ba00f01e2e7b136bbd8"}[0],
},
},
},
Expand Down Expand Up @@ -387,12 +399,40 @@ func TestGet(t *testing.T) {
"_conflicts": []string{"1-abc"},
},
},
{
name: "include only leaf 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)
}
_, err = d.Put(context.Background(), "foo", map[string]string{"foo": "qux"}, kivik.Rev("1-xyz"))
if err != nil {
t.Fatal(err)
}
},
id: "foo",
options: kivik.Param("conflicts", true),
wantDoc: map[string]interface{}{
"foo": "qux",
"_conflicts": []string{"1-abc"},
},
},
/*
TODO:
attachments = true
att_encoding_info = true
atts_since = [revs]
conflicts = true
deleted_conflicts = true
latest = true
local_seq = true
Expand Down Expand Up @@ -435,3 +475,87 @@ func TestGet(t *testing.T) {
})
}
}

func TestDelete(t *testing.T) {
t.Parallel()
tests := []struct {
name string
setup func(*testing.T, driver.DB)
id string
options driver.Options
wantRev string
check func(*testing.T, driver.DB)
wantStatus int
wantErr string
}{
{
name: "not found",
id: "foo",
wantStatus: http.StatusNotFound,
wantErr: "not found",
},
{
name: "success",
setup: func(t *testing.T, d driver.DB) {
_, err := d.Put(context.Background(), "foo", map[string]string{"foo": "bar"}, mock.NilOption)
if err != nil {
t.Fatal(err)
}
},
id: "foo",
options: kivik.Rev("1-9bb58f26192e4ba00f01e2e7b136bbd8"),
wantRev: "2-df2a4fe30cde39c357c8d1105748d1b9",
check: func(t *testing.T, d driver.DB) {
var deleted bool
err := d.(*db).db.QueryRow(`
SELECT deleted
FROM test
WHERE id='foo'
ORDER BY rev DESC, rev_id DESC
LIMIT 1
`).Scan(&deleted)
if err != nil {
t.Fatal(err)
}
if !deleted {
t.Errorf("Document not marked deleted")
}
},
},
/*
- delete already deleted doc -- 200 or 404?
- missing rev -- how does Couchdb respond?
*/
}

for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
db := newDB(t)
if tt.setup != nil {
tt.setup(t, db)
}
opts := tt.options
if opts == nil {
opts = mock.NilOption
}
rev, err := db.Delete(context.Background(), tt.id, opts)
if !testy.ErrorMatches(tt.wantErr, err) {
t.Errorf("Unexpected error: %s", err)
}
if status := kivik.HTTPStatus(err); status != tt.wantStatus {
t.Errorf("Unexpected status: %d", status)
}
if err != nil {
return
}
if rev != tt.wantRev {
t.Errorf("Unexpected rev: %s", rev)
}
if tt.check != nil {
tt.check(t, db)
}
})
}
}
3 changes: 3 additions & 0 deletions x/sqlite/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ type revision struct {
}

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

Expand Down
3 changes: 2 additions & 1 deletion x/sqlite/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ var schema = []string{
parent_rev INTEGER,
parent_rev_id TEXT,
FOREIGN KEY (id, parent_rev, parent_rev_id) REFERENCES %[2]q (id, rev, rev_id) ON DELETE CASCADE,
UNIQUE(id, rev, rev_id)
UNIQUE(id, rev, rev_id),
UNIQUE(id, parent_rev, parent_rev_id)
)`,
`CREATE TABLE %[1]q (
seq INTEGER PRIMARY KEY,
Expand Down
2 changes: 1 addition & 1 deletion x/sqlite/sqlite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import (
func TestNewClient(t *testing.T) {
d := drv{}

client, _ := d.NewClient("xxx", nil)
client, _ := d.NewClient(":memory:", nil)
if client == nil {
t.Fatal("client should not be nil")
}
Expand Down

0 comments on commit 33d1c07

Please sign in to comment.