diff --git a/clientv3/namespace/kv.go b/clientv3/namespace/kv.go index 5cc902899..35fe5858a 100644 --- a/clientv3/namespace/kv.go +++ b/clientv3/namespace/kv.go @@ -48,7 +48,7 @@ func (kv *kvPrefix) Put(ctx context.Context, key, val string, opts ...clientv3.O } func (kv *kvPrefix) Get(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.GetResponse, error) { - if len(key) == 0 { + if len(key) == 0 && !(clientv3.IsOptsWithFromKey(opts) || clientv3.IsOptsWithPrefix(opts)) { return nil, rpctypes.ErrEmptyKey } r, err := kv.KV.Do(ctx, kv.prefixOp(clientv3.OpGet(key, opts...))) @@ -61,7 +61,7 @@ func (kv *kvPrefix) Get(ctx context.Context, key string, opts ...clientv3.OpOpti } func (kv *kvPrefix) Delete(ctx context.Context, key string, opts ...clientv3.OpOption) (*clientv3.DeleteResponse, error) { - if len(key) == 0 { + if len(key) == 0 && !(clientv3.IsOptsWithFromKey(opts) || clientv3.IsOptsWithPrefix(opts)) { return nil, rpctypes.ErrEmptyKey } r, err := kv.KV.Do(ctx, kv.prefixOp(clientv3.OpDelete(key, opts...))) diff --git a/clientv3/op.go b/clientv3/op.go index 03b0cceaa..bd0f1f2f2 100644 --- a/clientv3/op.go +++ b/clientv3/op.go @@ -219,7 +219,7 @@ func (op Op) isWrite() bool { // OpGet returns "get" operation based on given key and operation options. func OpGet(key string, opts ...OpOption) Op { // WithPrefix and WithFromKey are not supported together - if isWithPrefix(opts) && isWithFromKey(opts) { + if IsOptsWithPrefix(opts) && IsOptsWithFromKey(opts) { panic("`WithPrefix` and `WithFromKey` cannot be set at the same time, choose one") } ret := Op{t: tRange, key: []byte(key)} @@ -230,7 +230,7 @@ func OpGet(key string, opts ...OpOption) Op { // OpDelete returns "delete" operation based on given key and operation options. func OpDelete(key string, opts ...OpOption) Op { // WithPrefix and WithFromKey are not supported together - if isWithPrefix(opts) && isWithFromKey(opts) { + if IsOptsWithPrefix(opts) && IsOptsWithFromKey(opts) { panic("`WithPrefix` and `WithFromKey` cannot be set at the same time, choose one") } ret := Op{t: tDeleteRange, key: []byte(key)} @@ -553,8 +553,8 @@ func toLeaseTimeToLiveRequest(id LeaseID, opts ...LeaseOption) *pb.LeaseTimeToLi return &pb.LeaseTimeToLiveRequest{ID: int64(id), Keys: ret.attachedKeys} } -// isWithPrefix returns true if WithPrefix is being called in the op -func isWithPrefix(opts []OpOption) bool { return isOpFuncCalled("WithPrefix", opts) } +// IsOptsWithPrefix returns true if WithPrefix option is called in the given opts. +func IsOptsWithPrefix(opts []OpOption) bool { return isOpFuncCalled("WithPrefix", opts) } -// isWithFromKey returns true if WithFromKey is being called in the op -func isWithFromKey(opts []OpOption) bool { return isOpFuncCalled("WithFromKey", opts) } +// IsOptsWithFromKey returns true if WithFromKey option is called in the given opts. +func IsOptsWithFromKey(opts []OpOption) bool { return isOpFuncCalled("WithFromKey", opts) } diff --git a/integration/v3_kv_test.go b/integration/v3_kv_test.go new file mode 100644 index 000000000..6e6683b03 --- /dev/null +++ b/integration/v3_kv_test.go @@ -0,0 +1,88 @@ +package integration + +import ( + "context" + "go.etcd.io/etcd/v3/clientv3" + "go.etcd.io/etcd/v3/clientv3/namespace" + "go.etcd.io/etcd/v3/embed" + "go.etcd.io/etcd/v3/etcdserver/api/v3client" + "go.etcd.io/etcd/v3/pkg/testutil" + "io/ioutil" + "os" + "testing" +) + +// TestKVWithEmptyValue ensures that a get/delete with an empty value, and with WithFromKey/WithPrefix function will return an empty error. +func TestKVWithEmptyValue(t *testing.T) { + defer testutil.AfterTest(t) + + cfg := embed.NewConfig() + + // Use temporary data directory. + dir, err := ioutil.TempDir("", "etcd-") + if err != nil { + panic(err) + } + defer os.RemoveAll(dir) + cfg.Dir = dir + + // Suppress server log to keep output clean. + //cfg.Logger = "zap" + //cfg.LogLevel = "error" + + etcd, err := embed.StartEtcd(cfg) + if err != nil { + panic(err) + } + defer etcd.Close() + <-etcd.Server.ReadyNotify() + + client := v3client.New(etcd.Server) + defer client.Close() + + _, err = client.Put(context.Background(), "my-namespace/foobar", "data") + if err != nil { + t.Fatal(err) + } + _, err = client.Put(context.Background(), "my-namespace/foobar1", "data") + if err != nil { + t.Fatal(err) + } + _, err = client.Put(context.Background(), "namespace/foobar1", "data") + if err != nil { + t.Fatal(err) + } + + // Range over all keys. + resp, err := client.Get(context.Background(), "", clientv3.WithFromKey()) + if err != nil { + t.Fatal(err) + } + for _, kv := range resp.Kvs { + t.Log(string(kv.Key), "=", string(kv.Value)) + } + + // Range over all keys in a namespace. + client.KV = namespace.NewKV(client.KV, "my-namespace/") + resp, err = client.Get(context.Background(), "", clientv3.WithFromKey()) + if err != nil { + t.Fatal(err) + } + for _, kv := range resp.Kvs { + t.Log(string(kv.Key), "=", string(kv.Value)) + } + + //Remove all keys without WithFromKey/WithPrefix func + respDel, err := client.Delete(context.Background(), "") + if err == nil { + // fatal error duo to without WithFromKey/WithPrefix func called. + t.Fatal(err) + } + + respDel, err = client.Delete(context.Background(), "", clientv3.WithFromKey()) + if err != nil { + // fatal error duo to with WithFromKey/WithPrefix func called. + t.Fatal(err) + } + t.Logf("delete keys:%d", respDel.Deleted) +}