Skip to content
This repository was archived by the owner on Nov 20, 2021. It is now read-only.

Commit f37d201

Browse files
author
Kyle Travis
committed
e2db: fix Delete issues
- Scan object while it is being deleted to delete entries in secondary/unique indexes for its current values. This requires that the object be retrieved from storage prior to deletion. - Add trailing slash to table key for prefixing. - Add test cases.
1 parent 0253ac6 commit f37d201

6 files changed

Lines changed: 199 additions & 85 deletions

File tree

pkg/e2db/db_test.go

Lines changed: 1 addition & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ func init() {
3131
RequiredClusterSize: 1,
3232
HealthCheckInterval: 1 * time.Second,
3333
HealthCheckTimeout: 5 * time.Second,
34+
EtcdLogLevel: zapcore.WarnLevel,
3435
})
3536
if err != nil {
3637
log.Fatal(err)
@@ -217,72 +218,3 @@ func TestUpdate(t *testing.T) {
217218
t.Errorf("e2db: after Update differs: (-want +got)\n%s", diff)
218219
}
219220
}
220-
221-
func TestDeletePrimaryIndex(t *testing.T) {
222-
resetTable(t)
223-
roles := db.Table(&Role{})
224-
225-
n, err := roles.Delete("ID", 10)
226-
if err != nil {
227-
t.Fatalf("unexpected error deleting non-existant key: %v", err)
228-
}
229-
if n != 0 {
230-
t.Fatalf("expected zero rows affected when deleting non-existant key, got %d", n)
231-
}
232-
233-
n, err = roles.Delete("ID", 1)
234-
if err != nil {
235-
t.Fatalf("unexpected error deleting by ID: %v", err)
236-
}
237-
if n != 1 {
238-
t.Fatalf("expected one row affected when deleting by ID, got %d", n)
239-
}
240-
241-
var r []*Role
242-
if err := roles.All(&r); err != nil {
243-
t.Fatal(err)
244-
}
245-
expected := []*Role{
246-
{ID: 2, Name: "admin", Description: "administrator"},
247-
{ID: 3, Name: "superadmin", Description: "administrator"},
248-
{ID: 4, Name: "smoot", Description: "administrator"},
249-
}
250-
if diff := cmp.Diff(expected, r); diff != "" {
251-
t.Errorf("e2db: after Update differs: (-want +got)\n%s", diff)
252-
}
253-
}
254-
255-
func TestDeleteSecondaryIndex(t *testing.T) {
256-
resetTable(t)
257-
roles := db.Table(&Role{})
258-
259-
n, err := roles.Delete("Name", "n/a")
260-
if err != nil {
261-
t.Fatalf("unexpected error deleting non-existant key: %v", err)
262-
}
263-
if n != 0 {
264-
t.Fatalf("expected zero rows affected when deleting non-existent key, got %d", n)
265-
}
266-
267-
n, err = roles.Delete("Name", "smoot")
268-
if err != nil {
269-
t.Fatalf("unexpected error deleting by Name: %v", err)
270-
}
271-
// NOTE(chris): includes value and index
272-
if n != 2 {
273-
t.Fatalf("expected one row affected when deleting by Name, got %d", n)
274-
}
275-
276-
var r []*Role
277-
if err := roles.All(&r); err != nil {
278-
t.Fatal(err)
279-
}
280-
expected := []*Role{
281-
{ID: 1, Name: "user", Description: "user"},
282-
{ID: 2, Name: "admin", Description: "administrator"},
283-
{ID: 3, Name: "superadmin", Description: "administrator"},
284-
}
285-
if diff := cmp.Diff(expected, r); diff != "" {
286-
t.Errorf("e2db: after Update differs: (-want +got)\n%s", diff)
287-
}
288-
}

pkg/e2db/key/key.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func ID(model, key string) string {
2727
}
2828

2929
func Table(model string) string {
30-
return join(model)
30+
return join(model) + "/"
3131
}
3232

3333
func TableDef(model string) string {

pkg/e2db/model.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ func (f *FieldDef) indexKey(tableName string, value string) (string, error) {
8282
type ModelDef struct {
8383
Name string
8484
Fields map[string]*FieldDef
85+
86+
t reflect.Type
8587
}
8688

8789
func NewModelDef(t reflect.Type) *ModelDef {
@@ -94,6 +96,7 @@ func NewModelDef(t reflect.Type) *ModelDef {
9496
m := &ModelDef{
9597
Name: t.Name(),
9698
Fields: make(map[string]*FieldDef),
99+
t: t,
97100
}
98101
for i := 0; i < t.NumField(); i++ {
99102
ft := t.Field(i)
@@ -116,6 +119,14 @@ func NewModelDef(t reflect.Type) *ModelDef {
116119
return m
117120
}
118121

122+
func (m *ModelDef) New() *reflect.Value {
123+
if m.t == nil {
124+
return nil
125+
}
126+
v := reflect.New(m.t)
127+
return &v
128+
}
129+
119130
type Field struct {
120131
*FieldDef
121132
value reflect.Value

pkg/e2db/query.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,14 @@ func (q *query) Find(fieldName string, data interface{}, to interface{}) error {
255255
}
256256
k := toString(data)
257257
if v.Type().Kind() == reflect.Slice {
258+
if f.isPrimaryKey() {
259+
item := reflect.New(v.Type().Elem())
260+
if err := q.findOneByPrimaryKey(key.ID(q.t.meta.Name, k), reflect.Indirect(item)); err != nil {
261+
return err
262+
}
263+
v.Set(reflect.Append(v, item.Elem()))
264+
return nil
265+
}
258266
return q.findManyByIndex(key.Indexes(q.t.meta.Name, f.Name, k), v)
259267
}
260268
switch f.Type() {

pkg/e2db/tx.go

Lines changed: 84 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package e2db
22

33
import (
44
"context"
5+
"fmt"
6+
"path/filepath"
57
"reflect"
68
"strings"
79
"time"
@@ -12,6 +14,7 @@ import (
1214
"github.com/pkg/errors"
1315
"go.etcd.io/etcd/clientv3"
1416
"go.etcd.io/etcd/etcdserver/etcdserverpb"
17+
"go.uber.org/zap"
1518
)
1619

1720
var (
@@ -196,36 +199,102 @@ func (tx *Tx) Update(iface interface{}) error {
196199
return err
197200
}
198201

202+
// getIndexesByPrimaryKey returns all index keys for the provided primary key
203+
// based on the stored value.
204+
func (tx *Tx) getIndexesByPrimaryKey(pk string) ([]string, error) {
205+
val := tx.meta.New()
206+
if val == nil {
207+
return nil, errors.Errorf("underlying type is uninitialized: %s", tx.meta.Name)
208+
}
209+
v := reflect.Indirect(reflect.ValueOf(val.Interface()))
210+
if err := newQuery(tx.Table).findOneByPrimaryKey(pk, v); err != nil {
211+
if errors.Cause(err) == ErrNoRows {
212+
return nil, nil
213+
}
214+
return nil, err
215+
}
216+
keys := []string{pk}
217+
_, id := filepath.Split(pk)
218+
for n, f := range tx.meta.Fields {
219+
switch f.Type() {
220+
case UniqueIndex:
221+
keys = append(keys, key.Unique(tx.meta.Name, n, toString(v.FieldByName(n).Interface())))
222+
case SecondaryIndex:
223+
keys = append(keys, key.Index(tx.meta.Name, n, toString(v.FieldByName(n).Interface()), id))
224+
}
225+
}
226+
return keys, nil
227+
}
228+
229+
func deleteOps(keys []string) (ops []clientv3.Op) {
230+
for _, k := range keys {
231+
ops = append(ops, clientv3.OpDelete(k))
232+
}
233+
return
234+
}
235+
199236
func (tx *Tx) Delete(fieldName string, data interface{}) (int64, error) {
237+
var n int64
238+
st := time.Now()
239+
defer func() {
240+
log.Debug("tx.Delete",
241+
zap.String("key", fmt.Sprintf("%s/%v", tx.meta.Name, fieldName)),
242+
zap.String("q", toString(data)),
243+
zap.Int64("n", n),
244+
zap.Duration("elapsed", time.Now().Sub(st)),
245+
)
246+
}()
200247
f, ok := tx.meta.Fields[fieldName]
201248
if !ok {
202249
return 0, errors.Errorf("invalid field name: %#v", fieldName)
203250
}
204251
k := toString(data)
205-
if f.isPrimaryKey() {
206-
resp, err := tx.db.client.Delete(context.TODO(), key.ID(tx.meta.Name, k))
252+
pks := make([]string, 0)
253+
254+
// get the primary key of the item(s) being deleted
255+
switch f.Type() {
256+
case PrimaryKey:
257+
pks = append(pks, key.ID(tx.meta.Name, k))
258+
case UniqueIndex:
259+
b, err := tx.db.client.Get(key.Unique(tx.meta.Name, fieldName, k))
207260
if err != nil {
261+
if errors.Cause(err) == client.ErrKeyNotFound {
262+
return 0, nil
263+
}
208264
return 0, err
209265
}
210-
return resp.Deleted, nil
211-
}
212-
kvs, err := tx.db.client.Prefix(key.Indexes(tx.meta.Name, fieldName, k))
213-
if err != nil {
214-
if errors.Cause(err) == client.ErrKeyNotFound {
215-
return 0, nil
266+
pks = append(pks, string(b))
267+
case SecondaryIndex:
268+
x := key.Indexes(tx.meta.Name, fieldName, k)
269+
kvs, err := tx.db.client.Prefix(x)
270+
if err != nil {
271+
if errors.Cause(err) == client.ErrKeyNotFound {
272+
return 0, nil
273+
}
274+
return 0, err
216275
}
217-
return 0, err
276+
for _, kv := range kvs {
277+
pks = append(pks, string(kv.Value))
278+
}
279+
default:
280+
return 0, errors.Wrapf(ErrNotIndexed, "cannot delete %#v", fieldName)
218281
}
282+
219283
ops := make([]clientv3.Op, 0)
220-
for _, kv := range kvs {
221-
ops = append(ops, clientv3.OpDelete(string(kv.Key)))
222-
ops = append(ops, clientv3.OpDelete(string(kv.Value)))
284+
for _, pk := range pks {
285+
keys, err := tx.getIndexesByPrimaryKey(pk)
286+
if err != nil {
287+
return 0, err
288+
}
289+
if len(keys) > 0 {
290+
n++
291+
}
292+
ops = append(ops, deleteOps(keys)...)
223293
}
224-
resp, err := tx.batchOps(ops...)
225-
if err != nil {
294+
if _, err := tx.batchOps(ops...); err != nil {
226295
return 0, err
227296
}
228-
return resp.Deleted, nil
297+
return n, nil
229298
}
230299

231300
func (tx *Tx) DeleteAll() error {

pkg/e2db/tx_test.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package e2db
33
import (
44
"testing"
55

6+
"github.com/google/go-cmp/cmp"
67
"github.com/pkg/errors"
78
)
89

@@ -24,3 +25,96 @@ func TestTxInsert(t *testing.T) {
2425
t.Fatal(err)
2526
}
2627
}
28+
29+
func TestTxDelete(t *testing.T) {
30+
cases := []struct {
31+
name string
32+
field string
33+
value interface{}
34+
filter func(*Role) bool
35+
n int64
36+
err error
37+
}{
38+
{
39+
name: "delete absent key",
40+
field: "ID",
41+
value: 20,
42+
filter: func(r *Role) bool { return r.ID == 20 },
43+
},
44+
{
45+
name: "delete primary key",
46+
field: "ID",
47+
value: 3,
48+
n: 1,
49+
filter: func(r *Role) bool { return r.ID == 3 },
50+
},
51+
{
52+
name: "delete secondary index",
53+
field: "Description",
54+
value: "administrator",
55+
n: 3,
56+
filter: func(r *Role) bool { return r.Description == "administrator" },
57+
},
58+
{
59+
name: "delete unique index",
60+
field: "Name",
61+
value: "smoot",
62+
n: 1,
63+
filter: func(r *Role) bool { return r.Name == "smoot" },
64+
},
65+
{
66+
name: "delete secondary index not found",
67+
field: "Description",
68+
value: "dinosaur",
69+
filter: func(r *Role) bool { return r.Description == "dinosaur" },
70+
},
71+
{
72+
name: "delete unique index not found",
73+
field: "Name",
74+
value: "smootest",
75+
filter: func(r *Role) bool { return r.Name == "smootest" },
76+
},
77+
{
78+
name: "delete non-index",
79+
field: "NotIndexed",
80+
value: "something",
81+
err: ErrNotIndexed,
82+
},
83+
// TODO(ktravis): make field/value a map to test multiple deletions in sequence
84+
}
85+
for _, c := range cases {
86+
t.Run(c.name, func(t *testing.T) {
87+
resetTable(t)
88+
roles := db.Table(&Role{})
89+
n, err := roles.Delete(c.field, c.value)
90+
if errors.Cause(err) != c.err {
91+
t.Fatalf("expected error %v, got %v", c.err, err)
92+
}
93+
if c.err == nil {
94+
if err != nil {
95+
t.Fatal(err)
96+
}
97+
} else {
98+
return
99+
}
100+
if n != c.n {
101+
t.Fatalf("expected delete count %d, got %d", c.n, n)
102+
}
103+
104+
expected := append([]*Role{}, newRoles...)
105+
for i := 0; i < len(expected); i++ {
106+
if c.filter(expected[i]) {
107+
expected = append(expected[:i], expected[i+1:]...)
108+
i--
109+
}
110+
}
111+
var result []*Role
112+
if err := roles.All(&result); err != nil {
113+
t.Fatal(err)
114+
}
115+
if diff := cmp.Diff(expected, result); diff != "" {
116+
t.Fatal("results did not match expected", diff)
117+
}
118+
})
119+
}
120+
}

0 commit comments

Comments
 (0)