From 5d65181f0afeb6be4b4712e9f6ba1d93b9152f44 Mon Sep 17 00:00:00 2001 From: xh3b4sd Date: Sat, 9 Dec 2023 15:44:36 +0100 Subject: [PATCH] add Sorted.Create.Union --- ...on_test.go => sentinel_connection_test.go} | 0 ...e_backup_test.go => single_backup_test.go} | 0 ...e_locker_test.go => single_locker_test.go} | 0 ...pub_sub_test.go => single_pub_sub_test.go} | 0 ...e_simple_test.go => single_simple_test.go} | 0 ...e_sorted_test.go => single_sorted_test.go} | 183 ++++++++++++++---- conformance/single_test.go | 21 ++ ...e_walker_test.go => single_walker_test.go} | 0 pkg/sorted/create/fake.go | 9 + pkg/sorted/create/redis.go | 45 ++++- pkg/sorted/interface.go | 15 ++ 11 files changed, 226 insertions(+), 47 deletions(-) rename conformance/{client_sentinel_connection_test.go => sentinel_connection_test.go} (100%) rename conformance/{client_single_backup_test.go => single_backup_test.go} (100%) rename conformance/{client_single_locker_test.go => single_locker_test.go} (100%) rename conformance/{client_single_pub_sub_test.go => single_pub_sub_test.go} (100%) rename conformance/{client_single_simple_test.go => single_simple_test.go} (100%) rename conformance/{client_single_sorted_test.go => single_sorted_test.go} (91%) create mode 100644 conformance/single_test.go rename conformance/{client_single_walker_test.go => single_walker_test.go} (100%) diff --git a/conformance/client_sentinel_connection_test.go b/conformance/sentinel_connection_test.go similarity index 100% rename from conformance/client_sentinel_connection_test.go rename to conformance/sentinel_connection_test.go diff --git a/conformance/client_single_backup_test.go b/conformance/single_backup_test.go similarity index 100% rename from conformance/client_single_backup_test.go rename to conformance/single_backup_test.go diff --git a/conformance/client_single_locker_test.go b/conformance/single_locker_test.go similarity index 100% rename from conformance/client_single_locker_test.go rename to conformance/single_locker_test.go diff --git a/conformance/client_single_pub_sub_test.go b/conformance/single_pub_sub_test.go similarity index 100% rename from conformance/client_single_pub_sub_test.go rename to conformance/single_pub_sub_test.go diff --git a/conformance/client_single_simple_test.go b/conformance/single_simple_test.go similarity index 100% rename from conformance/client_single_simple_test.go rename to conformance/single_simple_test.go diff --git a/conformance/client_single_sorted_test.go b/conformance/single_sorted_test.go similarity index 91% rename from conformance/client_single_sorted_test.go rename to conformance/single_sorted_test.go index cfa39ce..4331bd6 100644 --- a/conformance/client_single_sorted_test.go +++ b/conformance/single_sorted_test.go @@ -18,19 +18,7 @@ func Test_Client_Single_Sorted_Create_Order(t *testing.T) { var cli redigo.Interface { - c := redigo.Config{ - Kind: redigo.KindSingle, - } - - cli, err = redigo.New(c) - if err != nil { - t.Fatal(err) - } - - err = cli.Purge() - if err != nil { - t.Fatal(err) - } + cli = prgAll(redigo.Default()) } { @@ -91,79 +79,194 @@ func Test_Client_Single_Sorted_Create_Score(t *testing.T) { var cli redigo.Interface { - c := redigo.Config{ - Kind: redigo.KindSingle, - } - - cli, err = redigo.New(c) - if err != nil { - t.Fatal(err) - } - - err = cli.Purge() - if err != nil { - t.Fatal(err) - } + cli = prgAll(redigo.Default()) } { - err := cli.Sorted().Create().Index("ssk", "foo", 0.8, "a", "b") + err = cli.Sorted().Create().Index("ssk", "foo", 0.8, "a", "b") if err != nil { t.Fatal(err) } } { - err := cli.Sorted().Create().Index("ssk", "bar", 0.7, "c", "d") + err = cli.Sorted().Create().Index("ssk", "bar", 0.7, "c", "d") if err != nil { t.Fatal(err) } } { - err := cli.Sorted().Create().Index("ssk", "zap", 0.8, "e", "f") + err = cli.Sorted().Create().Index("ssk", "zap", 0.8, "e", "f") if !sorted.IsAlreadyExistsError(err) { t.Fatal("expected", "alreadyExistsError", "got", err) } } { - err := cli.Sorted().Create().Index("ssk", "foo", 0.8, "g", "h") + err = cli.Sorted().Create().Index("ssk", "foo", 0.8, "g", "h") if !sorted.IsAlreadyExistsError(err) { t.Fatal("expected", "alreadyExistsError", "got", err) } } } -func Test_Client_Single_Sorted_Create_Value(t *testing.T) { +func Test_Client_Single_Sorted_Create_Union(t *testing.T) { var err error var cli redigo.Interface { - c := redigo.Config{ - Kind: redigo.KindSingle, + cli = prgAll(redigo.Default()) + } + + { + res, err := cli.Sorted().Search().Union("k1", "k2") + if err != nil { + t.Fatal(err) + } + if len(res) != 0 { + t.Fatal("expected", 0, "got", len(res)) } + } - cli, err = redigo.New(c) + { + err = cli.Sorted().Create().Index("k1", "v3", 0.3) if err != nil { t.Fatal(err) } + err = cli.Sorted().Create().Index("k1", "v4", 0.4) + if err != nil { + t.Fatal(err) + } + err = cli.Sorted().Create().Index("k1", "v5", 0.5) + if err != nil { + t.Fatal(err) + } + err = cli.Sorted().Create().Index("k1", "v6", 0.6) + if err != nil { + t.Fatal(err) + } + } - err = cli.Purge() + { + res, err := cli.Sorted().Search().Union("k1") if err != nil { t.Fatal(err) } + if len(res) != 4 { + t.Fatal("expected", 4, "got", len(res)) + } + if res[0] != "v3" { + t.Fatal("expected", "v3", "got", res[0]) + } + if res[1] != "v4" { + t.Fatal("expected", "v4", "got", res[1]) + } + if res[2] != "v5" { + t.Fatal("expected", "v5", "got", res[2]) + } + if res[3] != "v6" { + t.Fatal("expected", "v6", "got", res[3]) + } } { - err := cli.Sorted().Create().Score("ssk", "foo", 0.8) + res, err := cli.Sorted().Search().Union("k1", "k2") if err != nil { t.Fatal(err) } + if len(res) != 4 { + t.Fatal("expected", 4, "got", len(res)) + } + if res[0] != "v3" { + t.Fatal("expected", "v3", "got", res[0]) + } + if res[1] != "v4" { + t.Fatal("expected", "v4", "got", res[1]) + } + if res[2] != "v5" { + t.Fatal("expected", "v5", "got", res[2]) + } + if res[3] != "v6" { + t.Fatal("expected", "v6", "got", res[3]) + } } { - err := cli.Sorted().Create().Score("ssk", "bar", 0.7) + err = cli.Sorted().Create().Index("k2", "v2", 0.2) + if err != nil { + t.Fatal(err) + } + err = cli.Sorted().Create().Index("k2", "v4", 0.4) + if err != nil { + t.Fatal(err) + } + err = cli.Sorted().Create().Index("k2", "v5", 0.5) + if err != nil { + t.Fatal(err) + } + err = cli.Sorted().Create().Index("k2", "v7", 0.7) + if err != nil { + t.Fatal(err) + } + } + + { + cou, err := cli.Sorted().Create().Union("k3", "k1", "k2") + if err != nil { + t.Fatal(err) + } + if cou != 6 { + t.Fatal("expected", 6, "got", cou) + } + } + + { + res, err := cli.Sorted().Search().Order("k3", 0, -1) + if err != nil { + t.Fatal(err) + } + if len(res) != 6 { + t.Fatal("expected", 6, "got", len(res)) + } + if res[0] != "v2" { + t.Fatal("expected", "v2", "got", res[0]) + } + if res[1] != "v3" { + t.Fatal("expected", "v3", "got", res[1]) + } + if res[2] != "v4" { + t.Fatal("expected", "v4", "got", res[2]) + } + if res[3] != "v5" { + t.Fatal("expected", "v5", "got", res[3]) + } + if res[4] != "v6" { + t.Fatal("expected", "v6", "got", res[4]) + } + if res[5] != "v7" { + t.Fatal("expected", "v7", "got", res[5]) + } + } +} + +func Test_Client_Single_Sorted_Create_Value(t *testing.T) { + var err error + + var cli redigo.Interface + { + cli = prgAll(redigo.Default()) + } + + { + err = cli.Sorted().Create().Score("ssk", "foo", 0.8) + if err != nil { + t.Fatal(err) + } + } + + { + err = cli.Sorted().Create().Score("ssk", "bar", 0.7) if err != nil { t.Fatal(err) } @@ -171,7 +274,7 @@ func Test_Client_Single_Sorted_Create_Value(t *testing.T) { // Verify we can create elements with duplicated scores. { - err := cli.Sorted().Create().Score("ssk", "zap", 0.8) + err = cli.Sorted().Create().Score("ssk", "zap", 0.8) if err != nil { t.Fatal(err) } @@ -179,7 +282,7 @@ func Test_Client_Single_Sorted_Create_Value(t *testing.T) { // Verify values must be unique after all. { - err := cli.Sorted().Create().Score("ssk", "foo", 0.8) + err = cli.Sorted().Create().Score("ssk", "foo", 0.8) if !sorted.IsAlreadyExistsError(err) { t.Fatal("expected", "alreadyExistsError", "got", err) } @@ -208,7 +311,7 @@ func Test_Client_Single_Sorted_Create_Value(t *testing.T) { // score. When foo is deleted, which has score 0.8 then zap must still exist // with the same score as we verify in the next step. { - err := cli.Sorted().Delete().Index("ssk", "foo") + err = cli.Sorted().Delete().Index("ssk", "foo") if err != nil { t.Fatal(err) } diff --git a/conformance/single_test.go b/conformance/single_test.go new file mode 100644 index 0000000..98046a3 --- /dev/null +++ b/conformance/single_test.go @@ -0,0 +1,21 @@ +//go:build single + +package conformance + +import ( + "github.com/xh3b4sd/redigo" + "github.com/xh3b4sd/tracer" +) + +// prgAll is a convenience function for calling FLUSHALL. The provided redigo +// interface is returned as is. +func prgAll(red redigo.Interface) redigo.Interface { + { + err := red.Purge() + if err != nil { + tracer.Panic(tracer.Mask(err)) + } + } + + return red +} diff --git a/conformance/client_single_walker_test.go b/conformance/single_walker_test.go similarity index 100% rename from conformance/client_single_walker_test.go rename to conformance/single_walker_test.go diff --git a/pkg/sorted/create/fake.go b/pkg/sorted/create/fake.go index 23b1585..2d572e3 100644 --- a/pkg/sorted/create/fake.go +++ b/pkg/sorted/create/fake.go @@ -3,6 +3,7 @@ package create type Fake struct { FakeIndex func() error FakeScore func() error + FakeUnion func() (int64, error) } func (f *Fake) Index(key string, val string, sco float64, ind ...string) error { @@ -20,3 +21,11 @@ func (f *Fake) Score(key string, val string, sco float64) error { return nil } + +func (f *Fake) Union(dst string, key ...string) (int64, error) { + if f.FakeUnion != nil { + return f.FakeUnion() + } + + return 0, nil +} diff --git a/pkg/sorted/create/redis.go b/pkg/sorted/create/redis.go index b1fbf47..0201546 100644 --- a/pkg/sorted/create/redis.go +++ b/pkg/sorted/create/redis.go @@ -39,8 +39,8 @@ func (r *Redis) Index(key string, val string, sco float64, ind ...string) error if len(ind) != 0 { m := map[string]int{} - for _, s := range ind { - m[s] = m[s] + 1 + for _, x := range ind { + m[x] = m[x] + 1 } for _, v := range m { @@ -49,11 +49,11 @@ func (r *Redis) Index(key string, val string, sco float64, ind ...string) error } } - for _, s := range ind { - if s == "" { + for _, x := range ind { + if x == "" { return tracer.Maskf(executionFailedError, "index must not be empty") } - if strings.Count(s, " ") != 0 { + if strings.Count(x, " ") != 0 { return tracer.Maskf(executionFailedError, "index must not contain whitespace") } } @@ -66,8 +66,8 @@ func (r *Redis) Index(key string, val string, sco float64, ind ...string) error arg = append(arg, val) // ARGV[1] arg = append(arg, sco) // ARGV[2] - for _, s := range ind { - arg = append(arg, s) + for _, x := range ind { + arg = append(arg, x) } } @@ -117,3 +117,34 @@ func (r *Redis) Score(key string, val string, sco float64) error { return nil } + +func (r *Redis) Union(dst string, key ...string) (int64, error) { + var err error + + var con redis.Conn + { + con = r.poo.Get() + defer con.Close() + } + + var arg []interface{} + { + arg = append(arg, dst, len(key)) + + for _, x := range key { + arg = append(arg, prefix.WithKeys(r.pre, x)) + } + + arg = append(arg, "AGGREGATE", "MIN") + } + + var res int64 + { + res, err = redis.Int64(con.Do("ZUNIONSTORE", arg...)) + if err != nil { + return 0, tracer.Mask(err) + } + } + + return res, nil +} diff --git a/pkg/sorted/interface.go b/pkg/sorted/interface.go index 20e22aa..404a5cc 100644 --- a/pkg/sorted/interface.go +++ b/pkg/sorted/interface.go @@ -24,6 +24,21 @@ type Create interface { // https://redis.io/commands/zadd // Score(key string, val string, sco float64) error + + // Union creates a new sorted set for the unique values that exist in any of + // the given keys. Therefore the destination values represent the union of the + // given keys. Given k1 and k2 hold the following values, Union(k1, k2) were + // to return v2, v3, v4, v5, v6 and v7. The resulting number of elements in + // dst is returned. + // + // k1 v3 v4 v5 v6 + // k2 v2 v4 v5 v7 + // + // For more information about the underlying behaviour see ZUNION. + // + // https://redis.io/commands/zunionstore + // + Union(dst string, key ...string) (int64, error) } type Delete interface {